mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e67feeea22 | |||
| da65474452 | |||
| d0a7e14a96 | |||
| bd4745775d | |||
| af144c6208 | |||
| 74206a947b | |||
| 457e12880c | |||
| 2d588949b1 | |||
| 84f7f901ea | |||
| 90ed503ee0 | |||
| 28105038d4 | |||
| fd60a2402a | |||
| ef0f21a11c | |||
| d0e40c1f1d | |||
| 8396e70e7b | |||
| 6128a24869 | |||
| 0329a6a7d3 | |||
| 2da89c2cf1 | |||
| 601ddf97ce | |||
| 128d031ced | |||
| 3b0205b25f | |||
| d4a47f1cce | |||
| 2998e3af3f | |||
| 5ff838d63e | |||
| bdaec30fa0 | |||
| 5a8bf9106e | |||
| 97df069730 | |||
| 9a1166f097 | |||
| 7a741e7ac4 | |||
| 165143a111 | |||
| ceb17b23b4 | |||
| 3c8c9d8b52 | |||
| 4162bccc46 | |||
| 24090fe350 | |||
| 37b78edb91 | |||
| 78510b6fd3 | |||
| edf99fcd1d | |||
| 2ffabd1ef8 | |||
| 28383edb83 | |||
| 9175897acf | |||
| f3cd167502 | |||
| 7cde2cf6c2 | |||
| 3913043705 | |||
| a160acef12 | |||
| c54fae0136 | |||
| d57abfcc93 | |||
| 515a0ddfdd | |||
| b5d8c65249 | |||
| de94892fe7 | |||
| b7131e16f2 | |||
| 11a44dc1fd | |||
| 0af718f03f | |||
| f8f5c3c6be | |||
| 13912a4af9 |
+1
-1
@@ -29,7 +29,7 @@ RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
|
|||||||
|
|
||||||
# Compile simplex-chat
|
# Compile simplex-chat
|
||||||
RUN cabal update
|
RUN cabal update
|
||||||
RUN cabal build exe:simplex-chat
|
RUN cabal build exe:simplex-chat --constraint 'simplexmq +client_library'
|
||||||
|
|
||||||
# Strip the binary from debug symbols to reduce size
|
# Strip the binary from debug symbols to reduce size
|
||||||
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
|
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
|
||||||
|
|||||||
+4
-2
@@ -25,7 +25,9 @@ While SimpleX Chat Ltd is not a communication service provider, and provide publ
|
|||||||
|
|
||||||
We see users and data sovereignty, and device and provider portability as critically important properties for any communication system.
|
We see users and data sovereignty, and device and provider portability as critically important properties for any communication system.
|
||||||
|
|
||||||
SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 – see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 – see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||||
|
|
||||||
|
The cryptographic review of SimpleX protocols design was done in July 2024 by Trail of Bits – see [the announcement](/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md).
|
||||||
|
|
||||||
### Your information
|
### Your information
|
||||||
|
|
||||||
@@ -172,4 +174,4 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b
|
|||||||
|
|
||||||
**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd.
|
**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd.
|
||||||
|
|
||||||
Updated April 24, 2024
|
Updated October 14, 2024
|
||||||
|
|||||||
@@ -233,14 +233,12 @@ You can use SimpleX with your own servers and still communicate with people usin
|
|||||||
|
|
||||||
Recent and important updates:
|
Recent and important updates:
|
||||||
|
|
||||||
|
[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md)
|
||||||
|
|
||||||
[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md)
|
[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md)
|
||||||
|
|
||||||
[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
|
[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
|
||||||
|
|
||||||
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
|
|
||||||
|
|
||||||
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
|
||||||
|
|
||||||
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
||||||
|
|
||||||
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
||||||
@@ -249,10 +247,6 @@ Recent and important updates:
|
|||||||
|
|
||||||
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
|
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
|
||||||
|
|
||||||
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
|
|
||||||
|
|
||||||
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
|
|
||||||
|
|
||||||
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
||||||
|
|
||||||
[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
|
[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||||
@@ -409,7 +403,9 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
|||||||
|
|
||||||
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
|
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
|
||||||
|
|
||||||
The security audit was performed in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about) – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||||
|
|
||||||
|
The cryptographic review of SimpleX protocols was done in July 2024 by Trail of Bits – see [the announcement](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md).
|
||||||
|
|
||||||
SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved.
|
SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved.
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ struct ContentView: View {
|
|||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@ObservedObject var alertManager = AlertManager.shared
|
@ObservedObject var alertManager = AlertManager.shared
|
||||||
@ObservedObject var callController = CallController.shared
|
@ObservedObject var callController = CallController.shared
|
||||||
|
@ObservedObject var appSheetState = AppSheetState.shared
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||||
@@ -250,7 +251,8 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private func mainView() -> some View {
|
private func mainView() -> some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ChatListView(activeUserPickerSheet: $chatListUserPickerSheet).privacySensitive(protectScreen)
|
ChatListView(activeUserPickerSheet: $chatListUserPickerSheet)
|
||||||
|
.redacted(reason: appSheetState.redactionReasons(protectScreen))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
requestNtfAuthorization()
|
requestNtfAuthorization()
|
||||||
// Local Authentication notice is to be shown on next start after onboarding is complete
|
// Local Authentication notice is to be shown on next start after onboarding is complete
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var chatDbEncrypted: Bool?
|
@Published var chatDbEncrypted: Bool?
|
||||||
@Published var chatDbStatus: DBMigrationResult?
|
@Published var chatDbStatus: DBMigrationResult?
|
||||||
@Published var ctrlInitInProgress: Bool = false
|
@Published var ctrlInitInProgress: Bool = false
|
||||||
|
@Published var notificationResponse: UNNotificationResponse?
|
||||||
// local authentication
|
// local authentication
|
||||||
@Published var contentViewAccessAuthenticated: Bool = false
|
@Published var contentViewAccessAuthenticated: Bool = false
|
||||||
@Published var laRequest: LocalAuthRequest?
|
@Published var laRequest: LocalAuthRequest?
|
||||||
|
|||||||
@@ -29,17 +29,33 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||||||
private var granted = false
|
private var granted = false
|
||||||
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
|
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
// Handle notification when app is in background
|
// Handle notification when app is in background
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||||
didReceive response: UNNotificationResponse,
|
didReceive response: UNNotificationResponse,
|
||||||
withCompletionHandler handler: () -> Void) {
|
withCompletionHandler handler: () -> Void) {
|
||||||
logger.debug("NtfManager.userNotificationCenter: didReceive")
|
logger.debug("NtfManager.userNotificationCenter: didReceive")
|
||||||
let content = response.notification.request.content
|
if appStateGroupDefault.get() == .active {
|
||||||
|
processNotificationResponse(response)
|
||||||
|
} else {
|
||||||
|
logger.debug("NtfManager.userNotificationCenter: remember response in model")
|
||||||
|
ChatModel.shared.notificationResponse = response
|
||||||
|
}
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func processNotificationResponse(_ ntfResponse: UNNotificationResponse) {
|
||||||
let chatModel = ChatModel.shared
|
let chatModel = ChatModel.shared
|
||||||
let action = response.actionIdentifier
|
let content = ntfResponse.notification.request.content
|
||||||
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
|
let action = ntfResponse.actionIdentifier
|
||||||
|
logger.debug("NtfManager.processNotificationResponse: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
|
||||||
if let userId = content.userInfo["userId"] as? Int64,
|
if let userId = content.userInfo["userId"] as? Int64,
|
||||||
userId != chatModel.currentUser?.userId {
|
userId != chatModel.currentUser?.userId {
|
||||||
|
logger.debug("NtfManager.processNotificationResponse changeActiveUser")
|
||||||
changeActiveUser(userId, viewPwd: nil)
|
changeActiveUser(userId, viewPwd: nil)
|
||||||
}
|
}
|
||||||
if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito),
|
if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito),
|
||||||
@@ -61,7 +77,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||||||
ItemsModel.shared.loadOpenChat(chatId)
|
ItemsModel.shared.loadOpenChat(chatId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ntfCallAction(_ content: UNNotificationContent, _ action: String) -> (ChatId, NtfCallAction)? {
|
private func ntfCallAction(_ content: UNNotificationContent, _ action: String) -> (ChatId, NtfCallAction)? {
|
||||||
@@ -76,7 +91,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Handle notification when the app is in foreground
|
// Handle notification when the app is in foreground
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||||
willPresent notification: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
@@ -185,6 +199,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||||||
actions: [],
|
actions: [],
|
||||||
intentIdentifiers: [],
|
intentIdentifiers: [],
|
||||||
hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification")
|
hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification")
|
||||||
|
),
|
||||||
|
UNNotificationCategory(
|
||||||
|
identifier: ntfCategoryManyEvents,
|
||||||
|
actions: [],
|
||||||
|
intentIdentifiers: [],
|
||||||
|
hiddenPreviewsBodyPlaceholder: NSLocalizedString("New events", comment: "notification")
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -210,29 +230,28 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
center.delegate = self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) {
|
func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) {
|
||||||
logger.debug("NtfManager.notifyContactRequest")
|
logger.debug("NtfManager.notifyContactRequest")
|
||||||
addNotification(createContactRequestNtf(user, contactRequest))
|
addNotification(createContactRequestNtf(user, contactRequest, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyContactConnected(_ user: any UserLike, _ contact: Contact) {
|
func notifyContactConnected(_ user: any UserLike, _ contact: Contact) {
|
||||||
logger.debug("NtfManager.notifyContactConnected")
|
logger.debug("NtfManager.notifyContactConnected")
|
||||||
addNotification(createContactConnectedNtf(user, contact))
|
addNotification(createContactConnectedNtf(user, contact, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) {
|
func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||||
logger.debug("NtfManager.notifyMessageReceived")
|
logger.debug("NtfManager.notifyMessageReceived")
|
||||||
if cInfo.ntfsEnabled {
|
if cInfo.ntfsEnabled {
|
||||||
addNotification(createMessageReceivedNtf(user, cInfo, cItem))
|
addNotification(createMessageReceivedNtf(user, cInfo, cItem, 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyCallInvitation(_ invitation: RcvCallInvitation) {
|
func notifyCallInvitation(_ invitation: RcvCallInvitation) {
|
||||||
logger.debug("NtfManager.notifyCallInvitation")
|
logger.debug("NtfManager.notifyCallInvitation")
|
||||||
addNotification(createCallInvitationNtf(invitation))
|
addNotification(createCallInvitationNtf(invitation, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func setNtfBadgeCount(_ count: Int) {
|
func setNtfBadgeCount(_ count: Int) {
|
||||||
|
|||||||
@@ -743,6 +743,12 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi
|
|||||||
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
|
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
|
||||||
)
|
)
|
||||||
return (nil, alert)
|
return (nil, alert)
|
||||||
|
case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))):
|
||||||
|
let alert = mkAlert(
|
||||||
|
title: "Undelivered messages",
|
||||||
|
message: "The connection reached the limit of undelivered messages, your contact may be offline."
|
||||||
|
)
|
||||||
|
return (nil, alert)
|
||||||
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
|
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
|
||||||
if internalErr == "SEUniqueID" {
|
if internalErr == "SEUniqueID" {
|
||||||
let alert = mkAlert(
|
let alert = mkAlert(
|
||||||
|
|||||||
@@ -82,11 +82,17 @@ struct SimpleXApp: App {
|
|||||||
|
|
||||||
if appState != .stopped {
|
if appState != .stopped {
|
||||||
startChatAndActivate {
|
startChatAndActivate {
|
||||||
if appState.inactive && chatModel.chatRunning == true {
|
if chatModel.chatRunning == true {
|
||||||
Task {
|
if let ntfResponse = chatModel.notificationResponse {
|
||||||
await updateChats()
|
chatModel.notificationResponse = nil
|
||||||
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
|
NtfManager.shared.processNotificationResponse(ntfResponse)
|
||||||
await updateCallInvitations()
|
}
|
||||||
|
if appState.inactive {
|
||||||
|
Task {
|
||||||
|
await updateChats()
|
||||||
|
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
|
||||||
|
await updateCallInvitations()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct CIChatFeatureView: View {
|
struct CIChatFeatureView: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@Environment(\.revealed) var revealed: Bool
|
||||||
@ObservedObject var im = ItemsModel.shared
|
@ObservedObject var im = ItemsModel.shared
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
@Binding var revealed: Bool
|
|
||||||
var feature: Feature
|
var feature: Feature
|
||||||
var icon: String? = nil
|
var icon: String? = nil
|
||||||
var iconColor: Color
|
var iconColor: Color
|
||||||
@@ -106,6 +106,9 @@ struct CIChatFeatureView: View {
|
|||||||
struct CIChatFeatureView_Previews: PreviewProvider {
|
struct CIChatFeatureView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let enabled = FeatureEnabled(forUser: false, forContact: false)
|
let enabled = FeatureEnabled(forUser: false, forContact: false)
|
||||||
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary))
|
CIChatFeatureView(
|
||||||
|
chat: Chat.sampleData,
|
||||||
|
chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary)
|
||||||
|
).environment(\.revealed, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,17 +285,18 @@ struct CIFileView_Previews: PreviewProvider {
|
|||||||
file: nil
|
file: nil
|
||||||
)
|
)
|
||||||
Group {
|
Group {
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: sentFile, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: sentFile)
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample())
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile)
|
||||||
}
|
}
|
||||||
|
.environment(\.revealed, false)
|
||||||
.previewLayout(.fixed(width: 360, height: 360))
|
.previewLayout(.fixed(width: 360, height: 360))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -510,10 +510,10 @@ struct CIVoiceView_Previews: PreviewProvider {
|
|||||||
duration: 30,
|
duration: 30,
|
||||||
allowMenu: Binding.constant(true)
|
allowMenu: Binding.constant(true)
|
||||||
)
|
)
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true))
|
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, allowMenu: .constant(true))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), allowMenu: .constant(true))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), allowMenu: .constant(true))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true))
|
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, allowMenu: .constant(true))
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 360, height: 360))
|
.previewLayout(.fixed(width: 360, height: 360))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,12 +92,13 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
|
|||||||
file: CIFile.getSample(fileStatus: .sndComplete)
|
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||||
)
|
)
|
||||||
Group {
|
Group {
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage)
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote)
|
||||||
}
|
}
|
||||||
|
.environment(\.revealed, false)
|
||||||
.previewLayout(.fixed(width: 360, height: 360))
|
.previewLayout(.fixed(width: 360, height: 360))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -12,9 +12,9 @@ import SimpleXChat
|
|||||||
struct MarkedDeletedItemView: View {
|
struct MarkedDeletedItemView: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.revealed) var revealed: Bool
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
@Binding var revealed: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
|
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
|
||||||
@@ -79,7 +79,10 @@ struct MarkedDeletedItemView: View {
|
|||||||
struct MarkedDeletedItemView_Previews: PreviewProvider {
|
struct MarkedDeletedItemView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
MarkedDeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true))
|
MarkedDeletedItemView(
|
||||||
|
chat: Chat.sampleData,
|
||||||
|
chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))
|
||||||
|
).environment(\.revealed, true)
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 360, height: 200))
|
.previewLayout(.fixed(width: 360, height: 200))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,28 @@ extension EnvironmentValues {
|
|||||||
static let defaultValue: Bool = true
|
static let defaultValue: Bool = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Revealed: EnvironmentKey {
|
||||||
|
static let defaultValue: Bool = true
|
||||||
|
}
|
||||||
|
|
||||||
var showTimestamp: Bool {
|
var showTimestamp: Bool {
|
||||||
get { self[ShowTimestamp.self] }
|
get { self[ShowTimestamp.self] }
|
||||||
set { self[ShowTimestamp.self] = newValue }
|
set { self[ShowTimestamp.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var revealed: Bool {
|
||||||
|
get { self[Revealed.self] }
|
||||||
|
set { self[Revealed.self] = newValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatItemView: View {
|
struct ChatItemView: View {
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||||
|
@Environment(\.revealed) var revealed: Bool
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
var maxWidth: CGFloat = .infinity
|
var maxWidth: CGFloat = .infinity
|
||||||
@Binding var revealed: Bool
|
|
||||||
@Binding var allowMenu: Bool
|
@Binding var allowMenu: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@@ -34,27 +43,25 @@ struct ChatItemView: View {
|
|||||||
chatItem: ChatItem,
|
chatItem: ChatItem,
|
||||||
showMember: Bool = false,
|
showMember: Bool = false,
|
||||||
maxWidth: CGFloat = .infinity,
|
maxWidth: CGFloat = .infinity,
|
||||||
revealed: Binding<Bool>,
|
|
||||||
allowMenu: Binding<Bool> = .constant(false)
|
allowMenu: Binding<Bool> = .constant(false)
|
||||||
) {
|
) {
|
||||||
self.chat = chat
|
self.chat = chat
|
||||||
self.chatItem = chatItem
|
self.chatItem = chatItem
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
_revealed = revealed
|
|
||||||
_allowMenu = allowMenu
|
_allowMenu = allowMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let ci = chatItem
|
let ci = chatItem
|
||||||
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
|
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
|
||||||
MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed)
|
MarkedDeletedItemView(chat: chat, chatItem: chatItem)
|
||||||
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
|
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
|
||||||
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
|
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
|
||||||
EmojiItemView(chat: chat, chatItem: ci)
|
EmojiItemView(chat: chat, chatItem: ci)
|
||||||
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
|
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
|
||||||
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu)
|
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu)
|
||||||
} else if ci.content.msgContent == nil {
|
} else if ci.content.msgContent == nil {
|
||||||
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||||
} else {
|
} else {
|
||||||
framedItemView()
|
framedItemView()
|
||||||
}
|
}
|
||||||
@@ -84,7 +91,6 @@ struct ChatItemView: View {
|
|||||||
chat: chat,
|
chat: chat,
|
||||||
chatItem: chatItem,
|
chatItem: chatItem,
|
||||||
preview: preview,
|
preview: preview,
|
||||||
revealed: $revealed,
|
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
imgWidth: adjustedMaxWidth,
|
imgWidth: adjustedMaxWidth,
|
||||||
videoWidth: adjustedMaxWidth,
|
videoWidth: adjustedMaxWidth,
|
||||||
@@ -96,9 +102,9 @@ struct ChatItemView: View {
|
|||||||
struct ChatItemContentView<Content: View>: View {
|
struct ChatItemContentView<Content: View>: View {
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.revealed) var revealed: Bool
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
@Binding var revealed: Bool
|
|
||||||
var msgContentView: () -> Content
|
var msgContentView: () -> Content
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
|
|
||||||
@@ -130,7 +136,7 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
case let .rcvChatPreference(feature, allowed, param):
|
case let .rcvChatPreference(feature, allowed, param):
|
||||||
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
||||||
case let .sndChatPreference(feature, _, _):
|
case let .sndChatPreference(feature, _, _):
|
||||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
|
CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
|
||||||
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
||||||
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
||||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||||
@@ -177,7 +183,7 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
||||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
|
CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mergedGroupEventText: Text? {
|
private var mergedGroupEventText: Text? {
|
||||||
@@ -238,16 +244,17 @@ func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
|
|||||||
struct ChatItemView_Previews: PreviewProvider {
|
struct ChatItemView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group{
|
Group{
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)))
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true)
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
|
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true)
|
||||||
}
|
}
|
||||||
|
.environment(\.revealed, false)
|
||||||
.previewLayout(.fixed(width: 360, height: 70))
|
.previewLayout(.fixed(width: 360, height: 70))
|
||||||
.environmentObject(Chat.sampleData)
|
.environmentObject(Chat.sampleData)
|
||||||
}
|
}
|
||||||
@@ -265,8 +272,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||||||
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)),
|
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)),
|
||||||
quotedItem: nil,
|
quotedItem: nil,
|
||||||
file: nil
|
file: nil
|
||||||
),
|
)
|
||||||
revealed: Binding.constant(true)
|
|
||||||
)
|
)
|
||||||
ChatItemView(
|
ChatItemView(
|
||||||
chat: Chat.sampleData,
|
chat: Chat.sampleData,
|
||||||
@@ -276,8 +282,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||||||
content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2),
|
content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2),
|
||||||
quotedItem: nil,
|
quotedItem: nil,
|
||||||
file: nil
|
file: nil
|
||||||
),
|
)
|
||||||
revealed: Binding.constant(true)
|
|
||||||
)
|
)
|
||||||
ChatItemView(
|
ChatItemView(
|
||||||
chat: Chat.sampleData,
|
chat: Chat.sampleData,
|
||||||
@@ -287,8 +292,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||||||
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin),
|
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin),
|
||||||
quotedItem: nil,
|
quotedItem: nil,
|
||||||
file: nil
|
file: nil
|
||||||
),
|
)
|
||||||
revealed: Binding.constant(true)
|
|
||||||
)
|
)
|
||||||
ChatItemView(
|
ChatItemView(
|
||||||
chat: Chat.sampleData,
|
chat: Chat.sampleData,
|
||||||
@@ -298,8 +302,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||||||
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
|
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
|
||||||
quotedItem: nil,
|
quotedItem: nil,
|
||||||
file: nil
|
file: nil
|
||||||
),
|
)
|
||||||
revealed: Binding.constant(true)
|
|
||||||
)
|
)
|
||||||
ChatItemView(
|
ChatItemView(
|
||||||
chat: Chat.sampleData,
|
chat: Chat.sampleData,
|
||||||
@@ -309,10 +312,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||||||
content: ciFeatureContent,
|
content: ciFeatureContent,
|
||||||
quotedItem: nil,
|
quotedItem: nil,
|
||||||
file: nil
|
file: nil
|
||||||
),
|
)
|
||||||
revealed: Binding.constant(true)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.environment(\.revealed, true)
|
||||||
.previewLayout(.fixed(width: 360, height: 70))
|
.previewLayout(.fixed(width: 360, height: 70))
|
||||||
.environmentObject(Chat.sampleData)
|
.environmentObject(Chat.sampleData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1183,9 +1183,9 @@ struct ChatView: View {
|
|||||||
chat: chat,
|
chat: chat,
|
||||||
chatItem: ci,
|
chatItem: ci,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
revealed: .constant(revealed),
|
|
||||||
allowMenu: $allowMenu
|
allowMenu: $allowMenu
|
||||||
)
|
)
|
||||||
|
.environment(\.revealed, revealed)
|
||||||
.environment(\.showTimestamp, itemSeparation.timestamp)
|
.environment(\.showTimestamp, itemSeparation.timestamp)
|
||||||
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
|
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
|
||||||
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
||||||
|
|||||||
@@ -287,8 +287,8 @@ struct ComposeView: View {
|
|||||||
// this is a workaround to fire an explicit event in certain cases
|
// this is a workaround to fire an explicit event in certain cases
|
||||||
@State private var stopPlayback: Bool = false
|
@State private var stopPlayback: Bool = false
|
||||||
|
|
||||||
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
@UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
@UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ struct SendMessageView: View {
|
|||||||
@State private var showCustomTimePicker = false
|
@State private var showCustomTimePicker = false
|
||||||
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
|
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
|
||||||
@State private var progressByTimeout = false
|
@State private var progressByTimeout = false
|
||||||
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
@UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
|
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
|
||||||
tableView: tableView
|
tableView: tableView
|
||||||
) { (tableView, indexPath, item) -> UITableViewCell? in
|
) { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
|
if indexPath.item > self.itemCount - 8 {
|
||||||
self.representer.loadPage()
|
self.representer.loadPage()
|
||||||
}
|
}
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
|
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
|
||||||
|
|||||||
@@ -121,9 +121,11 @@ struct ChatListView: View {
|
|||||||
UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
|
UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.sheet(item: $activeUserPickerSheet) {
|
.appSheet(
|
||||||
UserPickerSheetView(sheet: $0)
|
item: $activeUserPickerSheet,
|
||||||
}
|
onDismiss: { chatModel.laRequest = nil },
|
||||||
|
content: { UserPickerSheetView(sheet: $0) }
|
||||||
|
)
|
||||||
.onChange(of: activeUserPickerSheet) {
|
.onChange(of: activeUserPickerSheet) {
|
||||||
if $0 != nil {
|
if $0 != nil {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import SwiftUI
|
|||||||
class AppSheetState: ObservableObject {
|
class AppSheetState: ObservableObject {
|
||||||
static let shared = AppSheetState()
|
static let shared = AppSheetState()
|
||||||
@Published var scenePhaseActive: Bool = false
|
@Published var scenePhaseActive: Bool = false
|
||||||
|
|
||||||
|
func redactionReasons(_ protectScreen: Bool) -> RedactionReasons {
|
||||||
|
!protectScreen || scenePhaseActive
|
||||||
|
? RedactionReasons()
|
||||||
|
: RedactionReasons.placeholder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PrivacySensitive: ViewModifier {
|
private struct PrivacySensitive: ViewModifier {
|
||||||
@@ -19,11 +25,7 @@ private struct PrivacySensitive: ViewModifier {
|
|||||||
@ObservedObject var appSheetState: AppSheetState = AppSheetState.shared
|
@ObservedObject var appSheetState: AppSheetState = AppSheetState.shared
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if !protectScreen {
|
content.redacted(reason: appSheetState.redactionReasons(protectScreen))
|
||||||
content
|
|
||||||
} else {
|
|
||||||
content.privacySensitive(!appSheetState.scenePhaseActive).redacted(reason: .privacy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ struct UserWallpaperEditor: View {
|
|||||||
@State var themeModeOverride: ThemeModeOverride
|
@State var themeModeOverride: ThemeModeOverride
|
||||||
@State var applyToMode: DefaultThemeMode?
|
@State var applyToMode: DefaultThemeMode?
|
||||||
@State var showMore: Bool = false
|
@State var showMore: Bool = false
|
||||||
|
@State var showFileImporter: Bool = false
|
||||||
@Binding var globalThemeUsed: Bool
|
@Binding var globalThemeUsed: Bool
|
||||||
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
|
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
|
||||||
|
|
||||||
@@ -125,24 +126,27 @@ struct UserWallpaperEditor: View {
|
|||||||
|
|
||||||
CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) })
|
CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) })
|
||||||
|
|
||||||
ImportExportThemeSection(perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in
|
ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes)
|
||||||
let importedFromString = imported.wallpaper?.importFromString()
|
|
||||||
let importedType = importedFromString?.toAppWallpaper().type
|
|
||||||
let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get())
|
|
||||||
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
|
|
||||||
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, nil).colors
|
|
||||||
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
|
|
||||||
Task {
|
|
||||||
await MainActor.run {
|
|
||||||
themeModeOverride = res
|
|
||||||
}
|
|
||||||
await save(applyToMode, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
|
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.modifier(
|
||||||
|
ThemeImporter(isPresented: $showFileImporter) { imported in
|
||||||
|
let importedFromString = imported.wallpaper?.importFromString()
|
||||||
|
let importedType = importedFromString?.toAppWallpaper().type
|
||||||
|
let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get())
|
||||||
|
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
|
||||||
|
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, nil).colors
|
||||||
|
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
|
||||||
|
Task {
|
||||||
|
await MainActor.run {
|
||||||
|
themeModeOverride = res
|
||||||
|
}
|
||||||
|
await save(applyToMode, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
|
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
|
||||||
@@ -216,6 +220,7 @@ struct ChatWallpaperEditor: View {
|
|||||||
@State var themeModeOverride: ThemeModeOverride
|
@State var themeModeOverride: ThemeModeOverride
|
||||||
@State var applyToMode: DefaultThemeMode?
|
@State var applyToMode: DefaultThemeMode?
|
||||||
@State var showMore: Bool = false
|
@State var showMore: Bool = false
|
||||||
|
@State var showFileImporter: Bool = false
|
||||||
@Binding var globalThemeUsed: Bool
|
@Binding var globalThemeUsed: Bool
|
||||||
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
|
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
|
||||||
|
|
||||||
@@ -328,24 +333,27 @@ struct ChatWallpaperEditor: View {
|
|||||||
|
|
||||||
CustomizeThemeColorsSection(editColor: editColor)
|
CustomizeThemeColorsSection(editColor: editColor)
|
||||||
|
|
||||||
ImportExportThemeSection(perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in
|
ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes)
|
||||||
let importedFromString = imported.wallpaper?.importFromString()
|
|
||||||
let importedType = importedFromString?.toAppWallpaper().type
|
|
||||||
let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
|
||||||
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
|
|
||||||
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, ChatModel.shared.currentUser?.uiThemes).colors
|
|
||||||
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
|
|
||||||
Task {
|
|
||||||
await MainActor.run {
|
|
||||||
themeModeOverride = res
|
|
||||||
}
|
|
||||||
await save(applyToMode, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
|
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.modifier(
|
||||||
|
ThemeImporter(isPresented: $showFileImporter) { imported in
|
||||||
|
let importedFromString = imported.wallpaper?.importFromString()
|
||||||
|
let importedType = importedFromString?.toAppWallpaper().type
|
||||||
|
let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||||
|
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
|
||||||
|
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, ChatModel.shared.currentUser?.uiThemes).colors
|
||||||
|
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
|
||||||
|
Task {
|
||||||
|
await MainActor.run {
|
||||||
|
themeModeOverride = res
|
||||||
|
}
|
||||||
|
await save(applyToMode, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
|
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// UserDefault.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by user on 14/10/2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
public struct UserDefault<Value: Equatable>: DynamicProperty {
|
||||||
|
@StateObject private var observer = UserDefaultObserver()
|
||||||
|
let initialValue: Value
|
||||||
|
let key: String
|
||||||
|
let store: UserDefaults
|
||||||
|
|
||||||
|
public init(
|
||||||
|
wrappedValue: Value,
|
||||||
|
_ key: String,
|
||||||
|
store: UserDefaults = .standard
|
||||||
|
) {
|
||||||
|
self.initialValue = wrappedValue
|
||||||
|
self.key = key
|
||||||
|
self.store = store
|
||||||
|
}
|
||||||
|
|
||||||
|
public var wrappedValue: Value {
|
||||||
|
get {
|
||||||
|
// Observer can only be accessed after the property wrapper is installed in view (runtime exception)
|
||||||
|
observer.subscribe(to: key)
|
||||||
|
return store.object(forKey: key) as? Value ?? initialValue
|
||||||
|
}
|
||||||
|
nonmutating set {
|
||||||
|
store.set(newValue, forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UserDefaultObserver: ObservableObject {
|
||||||
|
private var subscribed = false
|
||||||
|
|
||||||
|
func subscribe(to key: String) {
|
||||||
|
if !subscribed {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(userDefaultsDidChange),
|
||||||
|
name: UserDefaults.didChangeNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
subscribed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func userDefaultsDidChange(_ notification: Notification) {
|
||||||
|
Task { @MainActor in objectWillChange.send() }
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit { NotificationCenter.default.removeObserver(self) }
|
||||||
|
}
|
||||||
@@ -105,7 +105,12 @@ struct TerminalView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
.navigationTitle("Chat console")
|
.toolbar {
|
||||||
|
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
Text("Chat console").font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
.modifier(ThemedBackground())
|
.modifier(ThemedBackground())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -367,13 +367,13 @@ struct ChatThemePreview: View {
|
|||||||
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
|
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
|
||||||
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
|
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
|
||||||
HStack {
|
HStack {
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: alice)
|
||||||
.modifier(ChatItemClipped(alice, tailVisible: true))
|
.modifier(ChatItemClipped(alice, tailVisible: true))
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: bob)
|
||||||
.modifier(ChatItemClipped(bob, tailVisible: true))
|
.modifier(ChatItemClipped(bob, tailVisible: true))
|
||||||
.frame(alignment: .trailing)
|
.frame(alignment: .trailing)
|
||||||
}
|
}
|
||||||
@@ -583,11 +583,14 @@ struct CustomizeThemeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportExportThemeSection(perChat: nil, perUser: nil, save: { theme in
|
ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: nil)
|
||||||
|
}
|
||||||
|
.modifier(
|
||||||
|
ThemeImporter(isPresented: $showFileImporter) { theme in
|
||||||
ThemeManager.saveAndApplyThemeOverrides(theme)
|
ThemeManager.saveAndApplyThemeOverrides(theme)
|
||||||
saveThemeToDatabase(nil)
|
saveThemeToDatabase(nil)
|
||||||
})
|
}
|
||||||
}
|
)
|
||||||
/// When changing app theme, user overrides are hidden. User overrides will be returned back after closing Appearance screen, see ThemeDestinationPicker()
|
/// When changing app theme, user overrides are hidden. User overrides will be returned back after closing Appearance screen, see ThemeDestinationPicker()
|
||||||
.interactiveDismissDisabled(true)
|
.interactiveDismissDisabled(true)
|
||||||
}
|
}
|
||||||
@@ -595,10 +598,9 @@ struct CustomizeThemeView: View {
|
|||||||
|
|
||||||
struct ImportExportThemeSection: View {
|
struct ImportExportThemeSection: View {
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Binding var showFileImporter: Bool
|
||||||
var perChat: ThemeModeOverride?
|
var perChat: ThemeModeOverride?
|
||||||
var perUser: ThemeModeOverrides?
|
var perUser: ThemeModeOverrides?
|
||||||
var save: (ThemeOverrides) -> Void
|
|
||||||
@State private var showFileImporter = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
@@ -626,39 +628,47 @@ struct ImportExportThemeSection: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Text("Import theme").foregroundColor(theme.colors.primary)
|
Text("Import theme").foregroundColor(theme.colors.primary)
|
||||||
}
|
}
|
||||||
.fileImporter(
|
}
|
||||||
isPresented: $showFileImporter,
|
}
|
||||||
allowedContentTypes: [.data/*.plainText*/],
|
}
|
||||||
allowsMultipleSelection: false
|
|
||||||
) { result in
|
struct ThemeImporter: ViewModifier {
|
||||||
if case let .success(files) = result, let fileURL = files.first {
|
@Binding var isPresented: Bool
|
||||||
do {
|
var save: (ThemeOverrides) -> Void
|
||||||
var fileSize: Int? = nil
|
|
||||||
if fileURL.startAccessingSecurityScopedResource() {
|
func body(content: Content) -> some View {
|
||||||
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
content.fileImporter(
|
||||||
fileSize = resourceValues.fileSize
|
isPresented: $isPresented,
|
||||||
}
|
allowedContentTypes: [.data/*.plainText*/],
|
||||||
if let fileSize = fileSize,
|
allowsMultipleSelection: false
|
||||||
// Same as Android/desktop
|
) { result in
|
||||||
fileSize <= 5_500_000 {
|
if case let .success(files) = result, let fileURL = files.first {
|
||||||
if let string = try? String(contentsOf: fileURL, encoding: .utf8), let theme: ThemeOverrides = decodeYAML("themeId: \(UUID().uuidString)\n" + string) {
|
do {
|
||||||
save(theme)
|
var fileSize: Int? = nil
|
||||||
logger.error("Saved theme from file")
|
if fileURL.startAccessingSecurityScopedResource() {
|
||||||
} else {
|
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||||
logger.error("Error decoding theme file")
|
fileSize = resourceValues.fileSize
|
||||||
}
|
|
||||||
fileURL.stopAccessingSecurityScopedResource()
|
|
||||||
} else {
|
|
||||||
fileURL.stopAccessingSecurityScopedResource()
|
|
||||||
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary)
|
|
||||||
AlertManager.shared.showAlertMsg(
|
|
||||||
title: "Large file!",
|
|
||||||
message: "Currently maximum supported file size is \(prettyMaxFileSize)."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
logger.error("Appearance fileImporter error \(error.localizedDescription)")
|
|
||||||
}
|
}
|
||||||
|
if let fileSize = fileSize,
|
||||||
|
// Same as Android/desktop
|
||||||
|
fileSize <= 5_500_000 {
|
||||||
|
if let string = try? String(contentsOf: fileURL, encoding: .utf8), let theme: ThemeOverrides = decodeYAML("themeId: \(UUID().uuidString)\n" + string) {
|
||||||
|
save(theme)
|
||||||
|
logger.error("Saved theme from file")
|
||||||
|
} else {
|
||||||
|
logger.error("Error decoding theme file")
|
||||||
|
}
|
||||||
|
fileURL.stopAccessingSecurityScopedResource()
|
||||||
|
} else {
|
||||||
|
fileURL.stopAccessingSecurityScopedResource()
|
||||||
|
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary)
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title: "Large file!",
|
||||||
|
message: "Currently maximum supported file size is \(prettyMaxFileSize)."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Appearance fileImporter error \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,7 +331,12 @@ struct SettingsView: View {
|
|||||||
chatDatabaseRow()
|
chatDatabaseRow()
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
MigrateFromDevice(showProgressOnSettings: $showProgress)
|
MigrateFromDevice(showProgressOnSettings: $showProgress)
|
||||||
.navigationTitle("Migrate device")
|
.toolbar {
|
||||||
|
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
Text("Migrate device").font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -26,14 +26,52 @@ enum NSENotification {
|
|||||||
case nse(UNMutableNotificationContent)
|
case nse(UNMutableNotificationContent)
|
||||||
case callkit(RcvCallInvitation)
|
case callkit(RcvCallInvitation)
|
||||||
case empty
|
case empty
|
||||||
case msgInfo(NtfMsgAckInfo)
|
}
|
||||||
|
|
||||||
var isCallInvitation: Bool {
|
public enum NSENotificationData {
|
||||||
|
case connectionEvent(_ user: User, _ connEntity: ConnectionEntity)
|
||||||
|
case contactConnected(_ user: any UserLike, _ contact: Contact)
|
||||||
|
case contactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest)
|
||||||
|
case messageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem)
|
||||||
|
case callInvitation(_ invitation: RcvCallInvitation)
|
||||||
|
case msgInfo(NtfMsgAckInfo)
|
||||||
|
case noNtf
|
||||||
|
|
||||||
|
var callInvitation: RcvCallInvitation? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .nse(ntf): ntf.categoryIdentifier == ntfCategoryCallInvitation
|
case let .callInvitation(invitation): invitation
|
||||||
case .callkit: true
|
default: nil
|
||||||
case .empty: false
|
}
|
||||||
case .msgInfo: false
|
}
|
||||||
|
|
||||||
|
func notificationContent(_ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
|
return switch self {
|
||||||
|
case let .connectionEvent(user, connEntity): createConnectionEventNtf(user, connEntity, badgeCount)
|
||||||
|
case let .contactConnected(user, contact): createContactConnectedNtf(user, contact, badgeCount)
|
||||||
|
case let .contactRequest(user, contactRequest): createContactRequestNtf(user, contactRequest, badgeCount)
|
||||||
|
case let .messageReceived(user, cInfo, cItem): createMessageReceivedNtf(user, cInfo, cItem, badgeCount)
|
||||||
|
case let .callInvitation(invitation): createCallInvitationNtf(invitation, badgeCount)
|
||||||
|
case .msgInfo: UNMutableNotificationContent()
|
||||||
|
case .noNtf: UNMutableNotificationContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationEvent: NSENotificationData? {
|
||||||
|
return switch self {
|
||||||
|
case .connectionEvent: self
|
||||||
|
case .contactConnected: self
|
||||||
|
case .contactRequest: self
|
||||||
|
case .messageReceived: self
|
||||||
|
case .callInvitation: self
|
||||||
|
case .msgInfo: nil
|
||||||
|
case .noNtf: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newMsgData: (any UserLike, ChatInfo)? {
|
||||||
|
return switch self {
|
||||||
|
case let .messageReceived(user, cInfo, _): (user, cInfo)
|
||||||
|
default: nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,9 +81,10 @@ enum NSENotification {
|
|||||||
// or when background notification is received.
|
// or when background notification is received.
|
||||||
class NSEThreads {
|
class NSEThreads {
|
||||||
static let shared = NSEThreads()
|
static let shared = NSEThreads()
|
||||||
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
|
static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
|
||||||
private var allThreads: Set<UUID> = []
|
private var allThreads: Set<UUID> = []
|
||||||
private var activeThreads: [(UUID, NotificationService)] = []
|
var activeThreads: [(UUID, NotificationService)] = []
|
||||||
|
var droppedNotifications: [(ChatId, NSENotificationData)] = []
|
||||||
|
|
||||||
func newThread() -> UUID {
|
func newThread() -> UUID {
|
||||||
NSEThreads.queue.sync {
|
NSEThreads.queue.sync {
|
||||||
@@ -64,22 +103,19 @@ class NSEThreads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void {
|
func processNotification(_ id: ChatId, _ ntf: NSENotificationData) async -> Void {
|
||||||
var waitTime: Int64 = 5_000_000000
|
if let (_, nse) = rcvEntityThread(id),
|
||||||
while waitTime > 0 {
|
nse.expectedMessages[id]?.shouldProcessNtf ?? false {
|
||||||
if let (_, nse) = rcvEntityThread(id),
|
nse.processReceivedNtf(id, ntf, signalReady: true)
|
||||||
nse.shouldProcessNtf && nse.processReceivedNtf(ntf) {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
try? await Task.sleep(nanoseconds: 10_000000)
|
|
||||||
waitTime -= 10_000000
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? {
|
private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? {
|
||||||
NSEThreads.queue.sync {
|
NSEThreads.queue.sync {
|
||||||
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id })
|
// this selects the earliest thread that:
|
||||||
|
// 1) has this connection in nse.expectedMessages
|
||||||
|
// 2) has not completed processing messages for this connection (not ready)
|
||||||
|
activeThreads.first(where: { (_, nse) in nse.expectedMessages[id]?.ready == false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,31 +142,38 @@ class NSEThreads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ExpectedMessage {
|
||||||
|
var ntfConn: UserNtfConn
|
||||||
|
var receiveConnId: String?
|
||||||
|
var expectedMsgId: String?
|
||||||
|
var allowedGetNextAttempts: Int
|
||||||
|
var msgBestAttemptNtf: NSENotificationData?
|
||||||
|
var ready: Bool
|
||||||
|
var shouldProcessNtf: Bool
|
||||||
|
var startedProcessingNewMsgs: Bool
|
||||||
|
var semaphore: DispatchSemaphore
|
||||||
|
}
|
||||||
|
|
||||||
// Notification service extension creates a new instance of the class and calls didReceive for each notification.
|
// Notification service extension creates a new instance of the class and calls didReceive for each notification.
|
||||||
// Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never
|
// Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never
|
||||||
// more than one process of notification service extension exists at a time.
|
// more than one process of notification service extension exists at a time.
|
||||||
// Soon after notification service delivers the last notification it is either suspended or terminated.
|
// Soon after notification service delivers the last notification it is either suspended or terminated.
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||||
var bestAttemptNtf: NSENotification?
|
// served as notification if no message attempts (msgBestAttemptNtf) could be produced
|
||||||
|
var serviceBestAttemptNtf: NSENotification?
|
||||||
var badgeCount: Int = 0
|
var badgeCount: Int = 0
|
||||||
// thread is added to allThreads here - if thread did not start chat,
|
// thread is added to allThreads here - if thread did not start chat,
|
||||||
// chat does not need to be suspended but NSE state still needs to be set to "suspended".
|
// chat does not need to be suspended but NSE state still needs to be set to "suspended".
|
||||||
var threadId: UUID? = NSEThreads.shared.newThread()
|
var threadId: UUID? = NSEThreads.shared.newThread()
|
||||||
var notificationInfo: NtfMessages?
|
var expectedMessages: Dictionary<String, ExpectedMessage> = [:] // key is receiveEntityId
|
||||||
var receiveEntityId: String?
|
|
||||||
var receiveConnId: String?
|
|
||||||
var expectedMessage: String?
|
|
||||||
var allowedGetNextAttempts: Int = 3
|
|
||||||
// return true if the message is taken - it prevents sending it to another NotificationService instance for processing
|
|
||||||
var shouldProcessNtf = false
|
|
||||||
var appSubscriber: AppSubscriber?
|
var appSubscriber: AppSubscriber?
|
||||||
var returnedSuspension = false
|
var returnedSuspension = false
|
||||||
|
|
||||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||||
logger.debug("DEBUGGING: NotificationService.didReceive")
|
logger.debug("DEBUGGING: NotificationService.didReceive")
|
||||||
let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() }
|
let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() }
|
||||||
setBestAttemptNtf(ntf)
|
setServiceBestAttemptNtf(ntf)
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
registerGroupDefaults()
|
registerGroupDefaults()
|
||||||
let appState = appStateGroupDefault.get()
|
let appState = appStateGroupDefault.get()
|
||||||
@@ -138,13 +181,11 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
switch appState {
|
switch appState {
|
||||||
case .stopped:
|
case .stopped:
|
||||||
setBadgeCount()
|
setBadgeCount()
|
||||||
setBestAttemptNtf(createAppStoppedNtf())
|
setServiceBestAttemptNtf(createAppStoppedNtf(badgeCount))
|
||||||
deliverBestAttemptNtf()
|
deliverBestAttemptNtf()
|
||||||
case .suspended:
|
case .suspended:
|
||||||
setBadgeCount()
|
|
||||||
receiveNtfMessages(request, contentHandler)
|
receiveNtfMessages(request, contentHandler)
|
||||||
case .suspending:
|
case .suspending:
|
||||||
setBadgeCount()
|
|
||||||
Task {
|
Task {
|
||||||
let state: AppState = await withCheckedContinuation { cont in
|
let state: AppState = await withCheckedContinuation { cont in
|
||||||
appSubscriber = appStateSubscriber { s in
|
appSubscriber = appStateSubscriber { s in
|
||||||
@@ -171,8 +212,9 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
deliverBestAttemptNtf()
|
deliverBestAttemptNtf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
case .active: contentHandler(UNMutableNotificationContent())
|
||||||
deliverBestAttemptNtf()
|
case .activating: contentHandler(UNMutableNotificationContent())
|
||||||
|
case .bgRefresh: contentHandler(UNMutableNotificationContent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,78 +234,165 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
if let t = threadId { NSEThreads.shared.startThread(t, self) }
|
if let t = threadId { NSEThreads.shared.startThread(t, self) }
|
||||||
let dbStatus = startChat()
|
let dbStatus = startChat()
|
||||||
if case .ok = dbStatus,
|
if case .ok = dbStatus,
|
||||||
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
let ntfConns = apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.receivedMsg_ == nil ? 0 : 1))")
|
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfConns ntfConns count = \(ntfConns.count)")
|
||||||
if let connEntity = ntfInfo.connEntity_ {
|
|
||||||
setBestAttemptNtf(
|
for ntfConn in ntfConns {
|
||||||
ntfInfo.ntfsEnabled
|
addExpectedMessage(ntfConn: ntfConn)
|
||||||
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity))
|
}
|
||||||
: .empty
|
|
||||||
)
|
let connIdsToGet = expectedMessages.compactMap { (id, _) in
|
||||||
if let id = connEntity.id, ntfInfo.expectedMsg_ != nil {
|
let started = NSEThreads.queue.sync {
|
||||||
notificationInfo = ntfInfo
|
let canStart = checkCanStart(id)
|
||||||
receiveEntityId = id
|
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: can start: \(canStart)") }
|
||||||
receiveConnId = connEntity.conn.agentConnId
|
if canStart {
|
||||||
let expectedMsgId = ntfInfo.expectedMsg_?.msgId
|
processDroppedNotifications(id)
|
||||||
let receivedMsgId = ntfInfo.receivedMsg_?.msgId
|
expectedMessages[id]?.startedProcessingNewMsgs = true
|
||||||
logger.debug("NotificationService: receiveNtfMessages: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private), receivedMsgId = \(receivedMsgId ?? "nil", privacy: .private)")
|
expectedMessages[id]?.shouldProcessNtf = true
|
||||||
expectedMessage = expectedMsgId
|
}
|
||||||
shouldProcessNtf = true
|
return canStart
|
||||||
return
|
}
|
||||||
|
if started {
|
||||||
|
return expectedMessages[id]?.receiveConnId
|
||||||
|
} else {
|
||||||
|
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) waiting on semaphore") }
|
||||||
|
expectedMessages[id]?.semaphore.wait()
|
||||||
|
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) proceeding after semaphore") }
|
||||||
|
Task {
|
||||||
|
NSEThreads.queue.sync {
|
||||||
|
processDroppedNotifications(id)
|
||||||
|
expectedMessages[id]?.startedProcessingNewMsgs = true
|
||||||
|
expectedMessages[id]?.shouldProcessNtf = true
|
||||||
|
}
|
||||||
|
if let connId = expectedMessages[id]?.receiveConnId {
|
||||||
|
let _ = getConnNtfMessage(connId: connId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !connIdsToGet.isEmpty {
|
||||||
|
if let r = apiGetConnNtfMessages(connIds: connIdsToGet) {
|
||||||
|
logger.debug("NotificationService: receiveNtfMessages: apiGetConnNtfMessages count = \(r.count)")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
} else if let dbStatus = dbStatus {
|
} else if let dbStatus = dbStatus {
|
||||||
setBestAttemptNtf(createErrorNtf(dbStatus))
|
setServiceBestAttemptNtf(createErrorNtf(dbStatus, badgeCount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deliverBestAttemptNtf()
|
deliverBestAttemptNtf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addExpectedMessage(ntfConn: UserNtfConn) {
|
||||||
|
if let connEntity = ntfConn.connEntity_,
|
||||||
|
let receiveEntityId = connEntity.id, ntfConn.expectedMsg_ != nil {
|
||||||
|
let expectedMsgId = ntfConn.expectedMsg_?.msgId
|
||||||
|
logger.debug("NotificationService: addExpectedMessage: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private)")
|
||||||
|
expectedMessages[receiveEntityId] = ExpectedMessage(
|
||||||
|
ntfConn: ntfConn,
|
||||||
|
receiveConnId: connEntity.conn.agentConnId,
|
||||||
|
expectedMsgId: expectedMsgId,
|
||||||
|
allowedGetNextAttempts: 3,
|
||||||
|
msgBestAttemptNtf: ntfConn.defaultBestAttemptNtf,
|
||||||
|
ready: false,
|
||||||
|
shouldProcessNtf: false,
|
||||||
|
startedProcessingNewMsgs: false,
|
||||||
|
semaphore: DispatchSemaphore(value: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCanStart(_ entityId: String) -> Bool {
|
||||||
|
return !NSEThreads.shared.activeThreads.contains(where: {
|
||||||
|
(tId, nse) in tId != threadId && nse.expectedMessages.contains(where: { $0.key == entityId })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDroppedNotifications(_ entityId: String) {
|
||||||
|
if !NSEThreads.shared.droppedNotifications.isEmpty {
|
||||||
|
let messagesToProcess = NSEThreads.shared.droppedNotifications.filter { (eId, _) in eId == entityId }
|
||||||
|
NSEThreads.shared.droppedNotifications.removeAll(where: { (eId, _) in eId == entityId })
|
||||||
|
for (index, (_, ntf)) in messagesToProcess.enumerated() {
|
||||||
|
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entity \(entityId, privacy: .private): processing dropped notification \(index, privacy: .private)") }
|
||||||
|
processReceivedNtf(entityId, ntf, signalReady: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func serviceExtensionTimeWillExpire() {
|
override func serviceExtensionTimeWillExpire() {
|
||||||
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
|
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
|
||||||
deliverBestAttemptNtf(urgent: true)
|
deliverBestAttemptNtf(urgent: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processReceivedNtf(_ ntf: NSENotification) -> Bool {
|
var expectingMoreMessages: Bool {
|
||||||
guard let ntfInfo = notificationInfo, let expectedMsgTs = ntfInfo.expectedMsg_?.msgTs else { return false }
|
!expectedMessages.allSatisfy { $0.value.ready }
|
||||||
if !ntfInfo.user.showNotifications {
|
}
|
||||||
self.setBestAttemptNtf(.empty)
|
|
||||||
|
func processReceivedNtf(_ id: ChatId, _ ntf: NSENotificationData, signalReady: Bool) {
|
||||||
|
guard let expectedMessage = expectedMessages[id] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let expectedMsgTs = expectedMessage.ntfConn.expectedMsg_?.msgTs else {
|
||||||
|
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||||
|
if signalReady { entityReady(id) }
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if case let .msgInfo(info) = ntf {
|
if case let .msgInfo(info) = ntf {
|
||||||
if info.msgId == expectedMessage {
|
if info.msgId == expectedMessage.expectedMsgId {
|
||||||
expectedMessage = nil
|
|
||||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): expected")
|
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): expected")
|
||||||
|
expectedMessages[id]?.expectedMsgId = nil
|
||||||
|
if signalReady { entityReady(id) }
|
||||||
self.deliverBestAttemptNtf()
|
self.deliverBestAttemptNtf()
|
||||||
return true
|
|
||||||
} else if let msgTs = info.msgTs_, msgTs > expectedMsgTs {
|
} else if let msgTs = info.msgTs_, msgTs > expectedMsgTs {
|
||||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one")
|
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one")
|
||||||
|
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||||
|
if signalReady { entityReady(id) }
|
||||||
self.deliverBestAttemptNtf()
|
self.deliverBestAttemptNtf()
|
||||||
return false
|
} else if (expectedMessages[id]?.allowedGetNextAttempts ?? 0) > 0, let receiveConnId = expectedMessages[id]?.receiveConnId {
|
||||||
} else if allowedGetNextAttempts > 0, let receiveConnId = receiveConnId {
|
|
||||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, get next message")
|
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, get next message")
|
||||||
allowedGetNextAttempts -= 1
|
expectedMessages[id]?.allowedGetNextAttempts -= 1
|
||||||
if let receivedMsg = apiGetConnNtfMessage(connId: receiveConnId) {
|
if let receivedMsg = getConnNtfMessage(connId: receiveConnId) {
|
||||||
logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)")
|
logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)")
|
||||||
return true
|
|
||||||
} else {
|
} else {
|
||||||
logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt")
|
logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt")
|
||||||
|
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||||
|
if signalReady { entityReady(id) }
|
||||||
self.deliverBestAttemptNtf()
|
self.deliverBestAttemptNtf()
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it")
|
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it")
|
||||||
|
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||||
|
if signalReady { entityReady(id) }
|
||||||
self.deliverBestAttemptNtf()
|
self.deliverBestAttemptNtf()
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
} else if ntfInfo.user.showNotifications {
|
} else if expectedMessage.ntfConn.user.showNotifications {
|
||||||
logger.debug("NotificationService processNtf: setting best attempt")
|
logger.debug("NotificationService processNtf: setting best attempt")
|
||||||
self.setBestAttemptNtf(ntf)
|
if ntf.notificationEvent != nil {
|
||||||
if ntf.isCallInvitation {
|
setBadgeCount()
|
||||||
self.deliverBestAttemptNtf()
|
|
||||||
}
|
}
|
||||||
return true
|
let prevBestAttempt = expectedMessages[id]?.msgBestAttemptNtf
|
||||||
|
if prevBestAttempt?.callInvitation != nil {
|
||||||
|
if ntf.callInvitation != nil { // replace with newer call
|
||||||
|
expectedMessages[id]?.msgBestAttemptNtf = ntf
|
||||||
|
} // otherwise keep call as best attempt
|
||||||
|
} else {
|
||||||
|
expectedMessages[id]?.msgBestAttemptNtf = ntf
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NSEThreads.shared.droppedNotifications.append((id, ntf))
|
||||||
|
if signalReady { entityReady(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityReady(_ entityId: ChatId) {
|
||||||
|
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: entity \(entityId, privacy: .private)") }
|
||||||
|
expectedMessages[entityId]?.ready = true
|
||||||
|
if let (tNext, nse) = NSEThreads.shared.activeThreads.first(where: { (_, nse) in nse.expectedMessages[entityId]?.startedProcessingNewMsgs == false }) {
|
||||||
|
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: signal next thread \(tNext, privacy: .private) for entity \(entityId, privacy: .private)") }
|
||||||
|
nse.expectedMessages[entityId]?.semaphore.signal()
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setBadgeCount() {
|
func setBadgeCount() {
|
||||||
@@ -271,37 +400,32 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
ntfBadgeCountGroupDefault.set(badgeCount)
|
ntfBadgeCountGroupDefault.set(badgeCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
|
func setServiceBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
|
||||||
setBestAttemptNtf(.nse(ntf))
|
logger.debug("NotificationService.setServiceBestAttemptNtf")
|
||||||
}
|
serviceBestAttemptNtf = .nse(ntf)
|
||||||
|
|
||||||
func setBestAttemptNtf(_ ntf: NSENotification) {
|
|
||||||
logger.debug("NotificationService.setBestAttemptNtf")
|
|
||||||
if case let .nse(notification) = ntf {
|
|
||||||
notification.badge = badgeCount as NSNumber
|
|
||||||
bestAttemptNtf = .nse(notification)
|
|
||||||
} else {
|
|
||||||
bestAttemptNtf = ntf
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deliverBestAttemptNtf(urgent: Bool = false) {
|
private func deliverBestAttemptNtf(urgent: Bool = false) {
|
||||||
logger.debug("NotificationService.deliverBestAttemptNtf")
|
if (urgent || !expectingMoreMessages) {
|
||||||
// stop processing other messages
|
logger.debug("NotificationService.deliverBestAttemptNtf")
|
||||||
shouldProcessNtf = false
|
// stop processing other messages
|
||||||
|
for (key, _) in expectedMessages {
|
||||||
|
expectedMessages[key]?.shouldProcessNtf = false
|
||||||
|
}
|
||||||
|
|
||||||
let suspend: Bool
|
let suspend: Bool
|
||||||
if let t = threadId {
|
if let t = threadId {
|
||||||
threadId = nil
|
threadId = nil
|
||||||
suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
|
suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
|
||||||
} else {
|
} else {
|
||||||
suspend = false
|
suspend = false
|
||||||
|
}
|
||||||
|
deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
|
||||||
}
|
}
|
||||||
deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) {
|
private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) {
|
||||||
if case .callkit = bestAttemptNtf {
|
if useCallKit() && expectedMessages.contains(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }) {
|
||||||
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit")
|
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit")
|
||||||
if urgent {
|
if urgent {
|
||||||
// suspending NSE even though there may be other notifications
|
// suspending NSE even though there may be other notifications
|
||||||
@@ -339,19 +463,13 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func deliverNotification() {
|
private func deliverNotification() {
|
||||||
if let handler = contentHandler, let ntf = bestAttemptNtf {
|
if let handler = contentHandler, let ntf = prepareNotification() {
|
||||||
contentHandler = nil
|
contentHandler = nil
|
||||||
bestAttemptNtf = nil
|
serviceBestAttemptNtf = nil
|
||||||
let deliver: (UNMutableNotificationContent?) -> Void = { ntf in
|
|
||||||
let useNtf = if let ntf = ntf {
|
|
||||||
appStateGroupDefault.get().running ? UNMutableNotificationContent() : ntf
|
|
||||||
} else {
|
|
||||||
UNMutableNotificationContent()
|
|
||||||
}
|
|
||||||
handler(useNtf)
|
|
||||||
}
|
|
||||||
switch ntf {
|
switch ntf {
|
||||||
case let .nse(content): deliver(content)
|
case let .nse(content):
|
||||||
|
content.badge = badgeCount as NSNumber
|
||||||
|
handler(content)
|
||||||
case let .callkit(invitation):
|
case let .callkit(invitation):
|
||||||
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
|
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
|
||||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||||
@@ -362,13 +480,85 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
"callTs": invitation.callTs.timeIntervalSince1970
|
"callTs": invitation.callTs.timeIntervalSince1970
|
||||||
]) { error in
|
]) { error in
|
||||||
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
|
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
|
||||||
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
|
handler(error == nil ? UNMutableNotificationContent() : createCallInvitationNtf(invitation, self.badgeCount))
|
||||||
}
|
}
|
||||||
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
|
case .empty:
|
||||||
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
|
handler(UNMutableNotificationContent()) // used to mute notifications that did not unsubscribe yet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func prepareNotification() -> NSENotification? {
|
||||||
|
if expectedMessages.isEmpty {
|
||||||
|
return serviceBestAttemptNtf
|
||||||
|
} else if let callNtfKV = expectedMessages.first(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }),
|
||||||
|
let callInv = callNtfKV.value.msgBestAttemptNtf?.callInvitation,
|
||||||
|
let callNtf = callNtfKV.value.msgBestAttemptNtf {
|
||||||
|
return useCallKit() ? .callkit(callInv) : .nse(callNtf.notificationContent(badgeCount))
|
||||||
|
} else {
|
||||||
|
let ntfEvents = expectedMessages.compactMap { $0.value.msgBestAttemptNtf?.notificationEvent }
|
||||||
|
if ntfEvents.isEmpty {
|
||||||
|
return .empty
|
||||||
|
} else if let ntfEvent = ntfEvents.count == 1 ? ntfEvents.first : nil {
|
||||||
|
return .nse(ntfEvent.notificationContent(badgeCount))
|
||||||
|
} else {
|
||||||
|
return .nse(createJointNtf(ntfEvents))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createJointNtf(_ ntfEvents: [NSENotificationData]) -> UNMutableNotificationContent {
|
||||||
|
let previewMode = ntfPreviewModeGroupDefault.get()
|
||||||
|
let newMsgsData: [(any UserLike, ChatInfo)] = ntfEvents.compactMap { $0.newMsgData }
|
||||||
|
if !newMsgsData.isEmpty, let userId = newMsgsData.first?.0.userId {
|
||||||
|
let newMsgsChats: [ChatInfo] = newMsgsData.map { $0.1 }
|
||||||
|
let uniqueChatsNames = uniqueNewMsgsChatsNames(newMsgsChats)
|
||||||
|
var body: String
|
||||||
|
if previewMode == .hidden {
|
||||||
|
body = String.localizedStringWithFormat(NSLocalizedString("New messages in %d chats", comment: "notification body"), uniqueChatsNames.count)
|
||||||
|
} else {
|
||||||
|
body = String.localizedStringWithFormat(NSLocalizedString("From: %@", comment: "notification body"), newMsgsChatsNamesStr(uniqueChatsNames))
|
||||||
|
}
|
||||||
|
return createNotification(
|
||||||
|
categoryIdentifier: ntfCategoryManyEvents,
|
||||||
|
title: NSLocalizedString("New messages", comment: "notification"),
|
||||||
|
body: body,
|
||||||
|
userInfo: ["userId": userId],
|
||||||
|
badgeCount: badgeCount
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return createNotification(
|
||||||
|
categoryIdentifier: ntfCategoryManyEvents,
|
||||||
|
title: NSLocalizedString("New events", comment: "notification"),
|
||||||
|
body: String.localizedStringWithFormat(NSLocalizedString("%d new events", comment: "notification body"), ntfEvents.count),
|
||||||
|
badgeCount: badgeCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uniqueNewMsgsChatsNames(_ newMsgsChats: [ChatInfo]) -> [String] {
|
||||||
|
var seenChatIds = Set<ChatId>()
|
||||||
|
var uniqueChatsNames: [String] = []
|
||||||
|
for chat in newMsgsChats {
|
||||||
|
if !seenChatIds.contains(chat.id) {
|
||||||
|
seenChatIds.insert(chat.id)
|
||||||
|
uniqueChatsNames.append(chat.chatViewName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueChatsNames
|
||||||
|
}
|
||||||
|
|
||||||
|
private func newMsgsChatsNamesStr(_ names: [String]) -> String {
|
||||||
|
return switch names.count {
|
||||||
|
case 1: names[0]
|
||||||
|
case 2: "\(names[0]) and \(names[1])"
|
||||||
|
case 3: "\(names[0] + ", " + names[1]) and \(names[2])"
|
||||||
|
default:
|
||||||
|
names.count > 3
|
||||||
|
? "\(names[0]), \(names[1]) and \(names.count - 2) other chats"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nseStateGroupDefault must not be used in NSE directly, only via this singleton
|
// nseStateGroupDefault must not be used in NSE directly, only via this singleton
|
||||||
@@ -582,28 +772,25 @@ func chatRecvMsg() async -> ChatResponse? {
|
|||||||
private let isInChina = SKStorefront().countryCode == "CHN"
|
private let isInChina = SKStorefront().countryCode == "CHN"
|
||||||
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
||||||
|
|
||||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)? {
|
||||||
logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
|
logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
|
||||||
switch res {
|
switch res {
|
||||||
case let .contactConnected(user, contact, _):
|
case let .contactConnected(user, contact, _):
|
||||||
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
|
return (contact.id, .contactConnected(user, contact))
|
||||||
// case let .contactConnecting(contact):
|
// case let .contactConnecting(contact):
|
||||||
// TODO profile update
|
// TODO profile update
|
||||||
case let .receivedContactRequest(user, contactRequest):
|
case let .receivedContactRequest(user, contactRequest):
|
||||||
return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest)))
|
return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest))
|
||||||
case let .newChatItems(user, chatItems):
|
case let .newChatItems(user, chatItems):
|
||||||
// Received items are created one at a time
|
// Received items are created one at a time
|
||||||
if let chatItem = chatItems.first {
|
if let chatItem = chatItems.first {
|
||||||
let cInfo = chatItem.chatInfo
|
let cInfo = chatItem.chatInfo
|
||||||
var cItem = chatItem.chatItem
|
var cItem = chatItem.chatItem
|
||||||
if !cInfo.ntfsEnabled {
|
|
||||||
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
|
|
||||||
}
|
|
||||||
if let file = cItem.autoReceiveFile() {
|
if let file = cItem.autoReceiveFile() {
|
||||||
cItem = autoReceiveFile(file) ?? cItem
|
cItem = autoReceiveFile(file) ?? cItem
|
||||||
}
|
}
|
||||||
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
let ntf: NSENotificationData = (cInfo.ntfsEnabled && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf
|
||||||
return cItem.showNotification ? (chatItem.chatId, ntf) : nil
|
return (chatItem.chatId, ntf)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -620,10 +807,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
|||||||
return nil
|
return nil
|
||||||
case let .callInvitation(invitation):
|
case let .callInvitation(invitation):
|
||||||
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
|
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
|
||||||
return (
|
return (invitation.contact.id, .callInvitation(invitation))
|
||||||
invitation.contact.id,
|
|
||||||
useCallKit() ? .callkit(invitation) : .nse(createCallInvitationNtf(invitation))
|
|
||||||
)
|
|
||||||
case let .ntfMessage(_, connEntity, ntfMessage):
|
case let .ntfMessage(_, connEntity, ntfMessage):
|
||||||
return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil }
|
return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil }
|
||||||
case .chatSuspended:
|
case .chatSuspended:
|
||||||
@@ -704,15 +888,15 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [UserNtfConn]? {
|
||||||
guard apiGetActiveUser() != nil else {
|
guard apiGetActiveUser() != nil else {
|
||||||
logger.debug("no active user")
|
logger.debug("no active user")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
let r = sendSimpleXCmd(.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||||
if case let .ntfMessages(user, connEntity_, expectedMsg_, receivedMsg_) = r, let user = user {
|
if case let .ntfConns(ntfConns) = r {
|
||||||
logger.debug("apiGetNtfMessage response ntfMessages: \(receivedMsg_ == nil ? 0 : 1)")
|
logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)")
|
||||||
return NtfMessages(user: user, connEntity_: connEntity_, expectedMsg_: expectedMsg_, receivedMsg_: receivedMsg_)
|
return ntfConns.compactMap { toUserNtfConn($0) }
|
||||||
} else if case let .chatCmdError(_, error) = r {
|
} else if case let .chatCmdError(_, error) = r {
|
||||||
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
||||||
} else {
|
} else {
|
||||||
@@ -721,17 +905,33 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetConnNtfMessage(connId: String) -> NtfMsgInfo? {
|
func toUserNtfConn(_ ntfConn: NtfConn) -> UserNtfConn? {
|
||||||
|
if let user = ntfConn.user_ {
|
||||||
|
return UserNtfConn(user: user, connEntity_: ntfConn.connEntity_, expectedMsg_: ntfConn.expectedMsg_)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiGetConnNtfMessages(connIds: [String]) -> [NtfMsgInfo?]? {
|
||||||
guard apiGetActiveUser() != nil else {
|
guard apiGetActiveUser() != nil else {
|
||||||
logger.debug("no active user")
|
logger.debug("no active user")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let r = sendSimpleXCmd(.apiGetConnNtfMessage(connId: connId))
|
let r = sendSimpleXCmd(.apiGetConnNtfMessages(connIds: connIds))
|
||||||
if case let .connNtfMessage(receivedMsg_) = r {
|
if case let .connNtfMessages(receivedMsgs) = r {
|
||||||
logger.debug("apiGetConnNtfMessage response receivedMsg_: \(receivedMsg_ == nil ? 0 : 1)")
|
logger.debug("apiGetConnNtfMessages response receivedMsgs: \(receivedMsgs.count)")
|
||||||
return receivedMsg_
|
return receivedMsgs
|
||||||
|
}
|
||||||
|
logger.debug("apiGetConnNtfMessages error: \(responseError(r))")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConnNtfMessage(connId: String) -> NtfMsgInfo? {
|
||||||
|
let r_ = apiGetConnNtfMessages(connIds: [connId])
|
||||||
|
if let r = r_, let receivedMsg = r.count == 1 ? r.first : nil {
|
||||||
|
return receivedMsg
|
||||||
}
|
}
|
||||||
logger.debug("apiGetConnNtfMessage error: \(responseError(r))")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,13 +969,33 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NtfMessages {
|
struct UserNtfConn {
|
||||||
var user: User
|
var user: User
|
||||||
var connEntity_: ConnectionEntity?
|
var connEntity_: ConnectionEntity?
|
||||||
var expectedMsg_: NtfMsgInfo?
|
var expectedMsg_: NtfMsgInfo?
|
||||||
var receivedMsg_: NtfMsgInfo?
|
|
||||||
|
|
||||||
var ntfsEnabled: Bool {
|
var defaultBestAttemptNtf: NSENotificationData {
|
||||||
user.showNotifications && (connEntity_?.ntfsEnabled ?? false)
|
return if !user.showNotifications {
|
||||||
|
.noNtf
|
||||||
|
} else if let connEntity = connEntity_ {
|
||||||
|
switch connEntity {
|
||||||
|
case let .rcvDirectMsgConnection(_, contact):
|
||||||
|
contact?.chatSettings.enableNtfs == .all
|
||||||
|
? .connectionEvent(user, connEntity)
|
||||||
|
: .noNtf
|
||||||
|
case let .rcvGroupMsgConnection(_, groupInfo, _):
|
||||||
|
groupInfo.chatSettings.enableNtfs == .all
|
||||||
|
? .connectionEvent(user, connEntity)
|
||||||
|
: .noNtf
|
||||||
|
case .sndFileConnection: .noNtf
|
||||||
|
case .rcvFileConnection: .noNtf
|
||||||
|
case let .userContactConnection(_, userContact):
|
||||||
|
userContact.groupId == nil
|
||||||
|
? .connectionEvent(user, connEntity)
|
||||||
|
: .noNtf
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
.noNtf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,11 @@
|
|||||||
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
|
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
|
||||||
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
|
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
|
||||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||||
|
643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; };
|
||||||
|
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */; };
|
||||||
|
643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; };
|
||||||
|
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */; };
|
||||||
|
643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; };
|
||||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
|
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
|
||||||
@@ -204,6 +209,7 @@
|
|||||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; };
|
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; };
|
||||||
CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; };
|
CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; };
|
||||||
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; };
|
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; };
|
||||||
|
CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */; };
|
||||||
CEDB245B2C9CD71800FBC5F6 /* StickyScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */; };
|
CEDB245B2C9CD71800FBC5F6 /* StickyScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */; };
|
||||||
CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; };
|
CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; };
|
||||||
CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; };
|
CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; };
|
||||||
@@ -223,11 +229,6 @@
|
|||||||
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
||||||
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
||||||
E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9962C5906FF007928CC /* InfoPlist.strings */; };
|
E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9962C5906FF007928CC /* InfoPlist.strings */; };
|
||||||
E5E997C92CBA891A00D7A2FA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C42CBA891A00D7A2FA /* libgmpxx.a */; };
|
|
||||||
E5E997CA2CBA891A00D7A2FA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C52CBA891A00D7A2FA /* libgmp.a */; };
|
|
||||||
E5E997CB2CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C62CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv-ghc9.6.3.a */; };
|
|
||||||
E5E997CC2CBA891A00D7A2FA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C72CBA891A00D7A2FA /* libffi.a */; };
|
|
||||||
E5E997CD2CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C82CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv.a */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -490,6 +491,11 @@
|
|||||||
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
|
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
|
||||||
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
|
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
|
||||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
|
643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; };
|
||||||
|
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
|
643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; };
|
||||||
|
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; sourceTree = "<group>"; };
|
||||||
|
643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = "<group>"; };
|
||||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||||
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
|
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
|
||||||
@@ -545,6 +551,7 @@
|
|||||||
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
|
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
|
||||||
CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = "<group>"; };
|
CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = "<group>"; };
|
||||||
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; };
|
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; };
|
||||||
|
CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; };
|
||||||
CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyScrollView.swift; sourceTree = "<group>"; };
|
CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyScrollView.swift; sourceTree = "<group>"; };
|
||||||
CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; };
|
CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; };
|
||||||
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -612,11 +619,6 @@
|
|||||||
E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
E5E997C42CBA891A00D7A2FA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
E5E997C52CBA891A00D7A2FA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
E5E997C62CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv-ghc9.6.3.a"; sourceTree = "<group>"; };
|
|
||||||
E5E997C72CBA891A00D7A2FA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
|
||||||
E5E997C82CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv.a"; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -655,14 +657,14 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E5E997C92CBA891A00D7A2FA /* libgmpxx.a in Frameworks */,
|
643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */,
|
||||||
E5E997CB2CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv-ghc9.6.3.a in Frameworks */,
|
643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */,
|
||||||
E5E997CA2CBA891A00D7A2FA /* libgmp.a in Frameworks */,
|
643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
E5E997CC2CBA891A00D7A2FA /* libffi.a in Frameworks */,
|
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||||
E5E997CD2CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv.a in Frameworks */,
|
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */,
|
||||||
|
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -739,11 +741,6 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E5E997C72CBA891A00D7A2FA /* libffi.a */,
|
|
||||||
E5E997C52CBA891A00D7A2FA /* libgmp.a */,
|
|
||||||
E5E997C42CBA891A00D7A2FA /* libgmpxx.a */,
|
|
||||||
E5E997C62CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv-ghc9.6.3.a */,
|
|
||||||
E5E997C82CBA891A00D7A2FA /* libHSsimplex-chat-6.1.0.9-3X73OucN19a19eYgUK66sv.a */,
|
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -802,6 +799,7 @@
|
|||||||
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */,
|
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */,
|
||||||
CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */,
|
CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */,
|
||||||
CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */,
|
CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */,
|
||||||
|
CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */,
|
||||||
);
|
);
|
||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -814,6 +812,11 @@
|
|||||||
5CC2C0FA2809BF11000C35E3 /* Localizable.strings */,
|
5CC2C0FA2809BF11000C35E3 /* Localizable.strings */,
|
||||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */,
|
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */,
|
||||||
5C764E5C279C70B7000C6508 /* Libraries */,
|
5C764E5C279C70B7000C6508 /* Libraries */,
|
||||||
|
643B3B422CCBEB080083A2CF /* libffi.a */,
|
||||||
|
643B3B442CCBEB080083A2CF /* libgmp.a */,
|
||||||
|
643B3B402CCBEB080083A2CF /* libgmpxx.a */,
|
||||||
|
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */,
|
||||||
|
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */,
|
||||||
5CA059C2279559F40002BEB4 /* Shared */,
|
5CA059C2279559F40002BEB4 /* Shared */,
|
||||||
5CDCAD462818589900503DA2 /* SimpleX NSE */,
|
5CDCAD462818589900503DA2 /* SimpleX NSE */,
|
||||||
CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
|
CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
|
||||||
@@ -909,10 +912,9 @@
|
|||||||
5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
|
5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */,
|
||||||
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
|
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
|
||||||
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
|
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
|
||||||
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
|
|
||||||
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
|
|
||||||
5CADE79929211BB900072E13 /* PreferencesView.swift */,
|
5CADE79929211BB900072E13 /* PreferencesView.swift */,
|
||||||
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
|
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
|
||||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
|
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
|
||||||
@@ -920,9 +922,6 @@
|
|||||||
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
|
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
|
||||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
||||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
|
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
|
||||||
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
|
|
||||||
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
|
|
||||||
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
|
|
||||||
5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
|
5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
|
||||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
||||||
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
|
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
|
||||||
@@ -1053,6 +1052,18 @@
|
|||||||
path = Database;
|
path = Database;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
|
||||||
|
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
|
||||||
|
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
|
||||||
|
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
|
||||||
|
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
|
||||||
|
);
|
||||||
|
path = NetworkAndServers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
6440CA01288AEC770062C672 /* Group */ = {
|
6440CA01288AEC770062C672 /* Group */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1482,6 +1493,7 @@
|
|||||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
|
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
|
||||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
|
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
|
||||||
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
|
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
|
||||||
|
CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */,
|
||||||
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
|
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
|
||||||
5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */,
|
5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */,
|
||||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
|
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
|
||||||
@@ -1899,7 +1911,7 @@
|
|||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -1924,7 +1936,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES_THIN;
|
LLVM_LTO = YES_THIN;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1948,7 +1960,7 @@
|
|||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -1973,7 +1985,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1989,11 +2001,11 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -2009,11 +2021,11 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -2034,7 +2046,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GCC_OPTIMIZATION_LEVEL = s;
|
GCC_OPTIMIZATION_LEVEL = s;
|
||||||
@@ -2049,7 +2061,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -2071,7 +2083,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_CODE_COVERAGE = NO;
|
ENABLE_CODE_COVERAGE = NO;
|
||||||
@@ -2086,7 +2098,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -2108,7 +2120,7 @@
|
|||||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -2134,7 +2146,7 @@
|
|||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -2159,7 +2171,7 @@
|
|||||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -2185,7 +2197,7 @@
|
|||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -2210,7 +2222,7 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -2225,7 +2237,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -2244,7 +2256,7 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 244;
|
CURRENT_PROJECT_VERSION = 245;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -2259,7 +2271,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 6.1;
|
MARKETING_VERSION = 6.1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ public enum ChatCommand {
|
|||||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||||
case apiDeleteToken(token: DeviceToken)
|
case apiDeleteToken(token: DeviceToken)
|
||||||
case apiGetNtfMessage(nonce: String, encNtfInfo: String)
|
case apiGetNtfConns(nonce: String, encNtfInfo: String)
|
||||||
case apiGetConnNtfMessage(connId: String)
|
case apiGetConnNtfMessages(connIds: [String])
|
||||||
case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile)
|
case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile)
|
||||||
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
|
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
|
||||||
case apiJoinGroup(groupId: Int64)
|
case apiJoinGroup(groupId: Int64)
|
||||||
@@ -214,8 +214,8 @@ public enum ChatCommand {
|
|||||||
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
||||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||||
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
|
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
|
||||||
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)"
|
case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)"
|
||||||
case let .apiGetConnNtfMessage(connId): return "/_ntf conn message \(connId)"
|
case let .apiGetConnNtfMessages(connIds): return "/_ntf conn messages \(connIds.joined(separator: ","))"
|
||||||
case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))"
|
case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))"
|
||||||
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
|
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
|
||||||
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
|
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
|
||||||
@@ -369,8 +369,8 @@ public enum ChatCommand {
|
|||||||
case .apiRegisterToken: return "apiRegisterToken"
|
case .apiRegisterToken: return "apiRegisterToken"
|
||||||
case .apiVerifyToken: return "apiVerifyToken"
|
case .apiVerifyToken: return "apiVerifyToken"
|
||||||
case .apiDeleteToken: return "apiDeleteToken"
|
case .apiDeleteToken: return "apiDeleteToken"
|
||||||
case .apiGetNtfMessage: return "apiGetNtfMessage"
|
case .apiGetNtfConns: return "apiGetNtfConns"
|
||||||
case .apiGetConnNtfMessage: return "apiGetConnNtfMessage"
|
case .apiGetConnNtfMessages: return "apiGetConnNtfMessages"
|
||||||
case .apiNewGroup: return "apiNewGroup"
|
case .apiNewGroup: return "apiNewGroup"
|
||||||
case .apiAddMember: return "apiAddMember"
|
case .apiAddMember: return "apiAddMember"
|
||||||
case .apiJoinGroup: return "apiJoinGroup"
|
case .apiJoinGroup: return "apiJoinGroup"
|
||||||
@@ -682,8 +682,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case callInvitations(callInvitations: [RcvCallInvitation])
|
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||||
case ntfTokenStatus(status: NtfTknStatus)
|
case ntfTokenStatus(status: NtfTknStatus)
|
||||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
|
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
|
||||||
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, expectedMsg_: NtfMsgInfo?, receivedMsg_: NtfMsgInfo?)
|
case ntfConns(ntfConns: [NtfConn])
|
||||||
case connNtfMessage(receivedMsg_: NtfMsgInfo?)
|
case connNtfMessages(receivedMsgs: [NtfMsgInfo?])
|
||||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
|
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
|
||||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||||
case contactDisabled(user: UserRef, contact: Contact)
|
case contactDisabled(user: UserRef, contact: Contact)
|
||||||
@@ -851,8 +851,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .callInvitations: return "callInvitations"
|
case .callInvitations: return "callInvitations"
|
||||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||||
case .ntfToken: return "ntfToken"
|
case .ntfToken: return "ntfToken"
|
||||||
case .ntfMessages: return "ntfMessages"
|
case .ntfConns: return "ntfConns"
|
||||||
case .connNtfMessage: return "connNtfMessage"
|
case .connNtfMessages: return "connNtfMessages"
|
||||||
case .ntfMessage: return "ntfMessage"
|
case .ntfMessage: return "ntfMessage"
|
||||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||||
case .contactDisabled: return "contactDisabled"
|
case .contactDisabled: return "contactDisabled"
|
||||||
@@ -1029,8 +1029,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .callInvitations(invs): return String(describing: invs)
|
case let .callInvitations(invs): return String(describing: invs)
|
||||||
case let .ntfTokenStatus(status): return String(describing: status)
|
case let .ntfTokenStatus(status): return String(describing: status)
|
||||||
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
||||||
case let .ntfMessages(u, connEntity, expectedMsg_, receivedMsg_): return withUser(u, "connEntity: \(String(describing: connEntity))\nexpectedMsg_: \(String(describing: expectedMsg_))\nreceivedMsg_: \(String(describing: receivedMsg_))")
|
case let .ntfConns(ntfConns): return String(describing: ntfConns)
|
||||||
case let .connNtfMessage(receivedMsg_): return "receivedMsg_: \(String(describing: receivedMsg_))"
|
case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))"
|
||||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||||
case let .contactDisabled(u, contact): return withUser(u, String(describing: contact))
|
case let .contactDisabled(u, contact): return withUser(u, String(describing: contact))
|
||||||
@@ -2088,6 +2088,7 @@ public enum ProtocolErrorType: Decodable, Hashable {
|
|||||||
case AUTH
|
case AUTH
|
||||||
case CRYPTO
|
case CRYPTO
|
||||||
case QUOTA
|
case QUOTA
|
||||||
|
case STORE(storeErr: String)
|
||||||
case NO_MSG
|
case NO_MSG
|
||||||
case LARGE_MSG
|
case LARGE_MSG
|
||||||
case EXPIRED
|
case EXPIRED
|
||||||
|
|||||||
@@ -2270,16 +2270,13 @@ public enum ConnectionEntity: Decodable, Hashable {
|
|||||||
case let .userContactConnection(entityConnection, _): entityConnection
|
case let .userContactConnection(entityConnection, _): entityConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NtfConn: Decodable, Hashable {
|
||||||
|
public var user_: User?
|
||||||
|
public var connEntity_: ConnectionEntity?
|
||||||
|
public var expectedMsg_: NtfMsgInfo?
|
||||||
|
|
||||||
public var ntfsEnabled: Bool {
|
|
||||||
switch self {
|
|
||||||
case let .rcvDirectMsgConnection(_, contact): return contact?.chatSettings.enableNtfs == .all
|
|
||||||
case let .rcvGroupMsgConnection(_, groupInfo, _): return groupInfo.chatSettings.enableNtfs == .all
|
|
||||||
case .sndFileConnection: return false
|
|
||||||
case .rcvFileConnection: return false
|
|
||||||
case let .userContactConnection(_, userContact): return userContact.groupId == nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct NtfMsgInfo: Decodable, Hashable {
|
public struct NtfMsgInfo: Decodable, Hashable {
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
|
|||||||
public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
|
public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
|
||||||
public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
|
public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
|
||||||
public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT"
|
public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT"
|
||||||
|
public let ntfCategoryManyEvents = "NTF_CAT_MANY_EVENTS"
|
||||||
public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
|
public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
|
||||||
|
|
||||||
public let appNotificationId = "chat.simplex.app.notification"
|
public let appNotificationId = "chat.simplex.app.notification"
|
||||||
|
|
||||||
let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification")
|
let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification")
|
||||||
|
|
||||||
public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
|
public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||||
return createNotification(
|
return createNotification(
|
||||||
categoryIdentifier: ntfCategoryContactRequest,
|
categoryIdentifier: ntfCategoryContactRequest,
|
||||||
@@ -34,11 +35,12 @@ public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: User
|
|||||||
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName
|
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName
|
||||||
),
|
),
|
||||||
targetContentIdentifier: nil,
|
targetContentIdentifier: nil,
|
||||||
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId]
|
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId],
|
||||||
|
badgeCount: badgeCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) -> UNMutableNotificationContent {
|
public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||||
return createNotification(
|
return createNotification(
|
||||||
categoryIdentifier: ntfCategoryContactConnected,
|
categoryIdentifier: ntfCategoryContactConnected,
|
||||||
@@ -51,12 +53,13 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact)
|
|||||||
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName
|
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName
|
||||||
),
|
),
|
||||||
targetContentIdentifier: contact.id,
|
targetContentIdentifier: contact.id,
|
||||||
userInfo: ["userId": user.userId]
|
userInfo: ["userId": user.userId],
|
||||||
// userInfo: ["chatId": contact.id, "contactId": contact.apiId]
|
// userInfo: ["chatId": contact.id, "contactId": contact.apiId]
|
||||||
|
badgeCount: badgeCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
|
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
let previewMode = ntfPreviewModeGroupDefault.get()
|
let previewMode = ntfPreviewModeGroupDefault.get()
|
||||||
var title: String
|
var title: String
|
||||||
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
|
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
|
||||||
@@ -69,12 +72,13 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _
|
|||||||
title: title,
|
title: title,
|
||||||
body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"),
|
body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"),
|
||||||
targetContentIdentifier: cInfo.id,
|
targetContentIdentifier: cInfo.id,
|
||||||
userInfo: ["userId": user.userId]
|
userInfo: ["userId": user.userId],
|
||||||
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
|
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
|
||||||
|
badgeCount: badgeCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent {
|
public func createCallInvitationNtf(_ invitation: RcvCallInvitation, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
let text = invitation.callType.media == .video
|
let text = invitation.callType.media == .video
|
||||||
? NSLocalizedString("Incoming video call", comment: "notification")
|
? NSLocalizedString("Incoming video call", comment: "notification")
|
||||||
: NSLocalizedString("Incoming audio call", comment: "notification")
|
: NSLocalizedString("Incoming audio call", comment: "notification")
|
||||||
@@ -84,11 +88,12 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl
|
|||||||
title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):",
|
title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):",
|
||||||
body: text,
|
body: text,
|
||||||
targetContentIdentifier: nil,
|
targetContentIdentifier: nil,
|
||||||
userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId]
|
userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId],
|
||||||
|
badgeCount: badgeCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity) -> UNMutableNotificationContent {
|
public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||||
var title: String
|
var title: String
|
||||||
var body: String? = nil
|
var body: String? = nil
|
||||||
@@ -118,11 +123,12 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit
|
|||||||
title: title,
|
title: title,
|
||||||
body: body,
|
body: body,
|
||||||
targetContentIdentifier: targetContentIdentifier,
|
targetContentIdentifier: targetContentIdentifier,
|
||||||
userInfo: ["userId": user.userId]
|
userInfo: ["userId": user.userId],
|
||||||
|
badgeCount: badgeCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificationContent {
|
public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
var title: String
|
var title: String
|
||||||
switch dbStatus {
|
switch dbStatus {
|
||||||
case .errorNotADatabase:
|
case .errorNotADatabase:
|
||||||
@@ -142,14 +148,16 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificati
|
|||||||
}
|
}
|
||||||
return createNotification(
|
return createNotification(
|
||||||
categoryIdentifier: ntfCategoryConnectionEvent,
|
categoryIdentifier: ntfCategoryConnectionEvent,
|
||||||
title: title
|
title: title,
|
||||||
|
badgeCount: badgeCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createAppStoppedNtf() -> UNMutableNotificationContent {
|
public func createAppStoppedNtf(_ badgeCount: Int) -> UNMutableNotificationContent {
|
||||||
return createNotification(
|
return createNotification(
|
||||||
categoryIdentifier: ntfCategoryConnectionEvent,
|
categoryIdentifier: ntfCategoryConnectionEvent,
|
||||||
title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification")
|
title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification"),
|
||||||
|
badgeCount: badgeCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,8 +167,15 @@ private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember
|
|||||||
: "#\(groupInfo.displayName) \(groupMember.chatViewName):"
|
: "#\(groupInfo.displayName) \(groupMember.chatViewName):"
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil,
|
public func createNotification(
|
||||||
targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent {
|
categoryIdentifier: String,
|
||||||
|
title: String,
|
||||||
|
subtitle: String? = nil,
|
||||||
|
body: String? = nil,
|
||||||
|
targetContentIdentifier: String? = nil,
|
||||||
|
userInfo: [AnyHashable : Any] = [:],
|
||||||
|
badgeCount: Int
|
||||||
|
) -> UNMutableNotificationContent {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.categoryIdentifier = categoryIdentifier
|
content.categoryIdentifier = categoryIdentifier
|
||||||
content.title = title
|
content.title = title
|
||||||
@@ -170,6 +185,7 @@ public func createNotification(categoryIdentifier: String, title: String, subtit
|
|||||||
content.userInfo = userInfo
|
content.userInfo = userInfo
|
||||||
// TODO move logic of adding sound here, so it applies to background notifications too
|
// TODO move logic of adding sound here, so it applies to background notifications too
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
|
content.badge = badgeCount as NSNumber
|
||||||
// content.interruptionLevel = .active
|
// content.interruptionLevel = .active
|
||||||
// content.relevanceScore = 0.5 // 0-1
|
// content.relevanceScore = 0.5 // 0-1
|
||||||
return content
|
return content
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.*
|
||||||
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import chat.simplex.app.model.NtfManager
|
import chat.simplex.app.model.NtfManager
|
||||||
@@ -13,7 +15,6 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent
|
|||||||
import chat.simplex.common.*
|
import chat.simplex.common.*
|
||||||
import chat.simplex.common.helpers.*
|
import chat.simplex.common.helpers.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.chatlist.*
|
import chat.simplex.common.views.chatlist.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -24,13 +25,21 @@ import kotlinx.coroutines.*
|
|||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class MainActivity: FragmentActivity() {
|
class MainActivity: FragmentActivity() {
|
||||||
|
companion object {
|
||||||
|
const val OLD_ANDROID_UI_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
mainActivity = WeakReference(this)
|
mainActivity = WeakReference(this)
|
||||||
platform.androidSetNightModeIfSupported()
|
platform.androidSetNightModeIfSupported()
|
||||||
val c = CurrentColors.value.colors
|
val c = CurrentColors.value.colors
|
||||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
|
||||||
applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
|
applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
|
||||||
|
// This flag makes status bar and navigation bar fully transparent. But on API level < 30 it breaks insets entirely
|
||||||
|
// https://issuetracker.google.com/issues/236862874
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
// testJson()
|
// testJson()
|
||||||
// When call ended and orientation changes, it re-process old intent, it's unneeded.
|
// When call ended and orientation changes, it re-process old intent, it's unneeded.
|
||||||
@@ -47,6 +56,7 @@ class MainActivity: FragmentActivity() {
|
|||||||
WindowManager.LayoutParams.FLAG_SECURE
|
WindowManager.LayoutParams.FLAG_SECURE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
AppScreen()
|
AppScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import chat.simplex.common.platform.Log
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.os.*
|
import android.os.*
|
||||||
|
import android.view.View
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -16,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
|
import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS
|
||||||
import chat.simplex.app.model.NtfManager
|
import chat.simplex.app.model.NtfManager
|
||||||
import chat.simplex.app.model.NtfManager.AcceptCallAction
|
import chat.simplex.app.model.NtfManager.AcceptCallAction
|
||||||
import chat.simplex.app.views.call.CallActivity
|
import chat.simplex.app.views.call.CallActivity
|
||||||
@@ -26,7 +28,6 @@ import chat.simplex.common.model.ChatModel.withChats
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
import chat.simplex.common.views.chatlist.statusBarColorAfterCall
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
@@ -274,79 +275,32 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
uiModeManager.setApplicationNightMode(mode)
|
uiModeManager.setApplicationNightMode(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidSetDrawerStatusAndNavBarColor(
|
override fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean, themeBackgroundColor: Color) {
|
||||||
isLight: Boolean,
|
|
||||||
drawerShadingColor: Color,
|
|
||||||
toolbarOnTop: Boolean,
|
|
||||||
navBarColor: Color,
|
|
||||||
) {
|
|
||||||
val window = mainActivity.get()?.window ?: return
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
|
|
||||||
// Blend status bar color to the animated color
|
|
||||||
val colors = CurrentColors.value.colors
|
|
||||||
val baseBackgroundColor = if (toolbarOnTop) colors.background.mixWith(colors.onBackground, 0.97f) else colors.background
|
|
||||||
var statusBar = baseBackgroundColor.mixWith(drawerShadingColor.copy(1f), 1 - drawerShadingColor.alpha).toArgb()
|
|
||||||
var statusBarLight = isLight
|
|
||||||
|
|
||||||
// SimplexGreen while in call
|
|
||||||
if (window.statusBarColor == SimplexGreen.toArgb()) {
|
|
||||||
statusBarColorAfterCall.intValue = statusBar
|
|
||||||
statusBar = SimplexGreen.toArgb()
|
|
||||||
statusBarLight = false
|
|
||||||
}
|
|
||||||
window.statusBarColor = statusBar
|
|
||||||
val navBar = navBarColor.toArgb()
|
|
||||||
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
|
|
||||||
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
|
|
||||||
}
|
|
||||||
if (window.navigationBarColor != navBar) {
|
|
||||||
window.navigationBarColor = navBar
|
|
||||||
}
|
|
||||||
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
|
|
||||||
windowInsetController?.isAppearanceLightNavigationBars = isLight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {
|
|
||||||
val window = mainActivity.get()?.window ?: return
|
val window = mainActivity.get()?.window ?: return
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
val statusLight = isLightStatusBar && chatModel.activeCall.value == null
|
||||||
|
val navBarLight = isLightNavBar || windowOrientation() == WindowOrientation.LANDSCAPE
|
||||||
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
|
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
|
||||||
|
if (windowInsetController?.isAppearanceLightStatusBars != statusLight) {
|
||||||
var statusBar = (if (hasTop && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
windowInsetController?.isAppearanceLightStatusBars = statusLight
|
||||||
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
|
}
|
||||||
} else {
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
if (CurrentColors.value.base == DefaultTheme.SIMPLEX) {
|
if (windowInsetController?.isAppearanceLightNavigationBars != navBarLight) {
|
||||||
backgroundColor.lighter(0.4f)
|
windowInsetController?.isAppearanceLightNavigationBars = navBarLight
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
window.decorView.systemUiVisibility = if (statusLight && navBarLight) {
|
||||||
|
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS
|
||||||
|
} else if (statusLight) {
|
||||||
|
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or OLD_ANDROID_UI_FLAGS
|
||||||
|
} else if (navBarLight) {
|
||||||
|
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS
|
||||||
} else {
|
} else {
|
||||||
backgroundColor
|
OLD_ANDROID_UI_FLAGS
|
||||||
}
|
}
|
||||||
}).toArgb()
|
window.navigationBarColor = if (blackNavBar) Color.Black.toArgb() else themeBackgroundColor.toArgb()
|
||||||
var statusBarLight = isLight
|
|
||||||
|
|
||||||
// SimplexGreen while in call
|
|
||||||
if (window.statusBarColor == SimplexGreen.toArgb()) {
|
|
||||||
statusBarColorAfterCall.intValue = statusBar
|
|
||||||
statusBar = SimplexGreen.toArgb()
|
|
||||||
statusBarLight = false
|
|
||||||
}
|
|
||||||
val navBar = (if (hasBottom && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
|
||||||
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
|
|
||||||
} else {
|
} else {
|
||||||
backgroundColor
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
}).toArgb()
|
|
||||||
if (window.statusBarColor != statusBar) {
|
|
||||||
window.statusBarColor = statusBar
|
|
||||||
}
|
|
||||||
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
|
|
||||||
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
|
|
||||||
}
|
|
||||||
if (window.navigationBarColor != navBar) {
|
|
||||||
window.navigationBarColor = navBar
|
|
||||||
}
|
|
||||||
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
|
|
||||||
windowInsetController?.isAppearanceLightNavigationBars = isLight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +355,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ kotlin {
|
|||||||
implementation("androidx.activity:activity-compose:1.9.1")
|
implementation("androidx.activity:activity-compose:1.9.1")
|
||||||
val workVersion = "2.9.1"
|
val workVersion = "2.9.1"
|
||||||
implementation("androidx.work:work-runtime-ktx:$workVersion")
|
implementation("androidx.work:work-runtime-ktx:$workVersion")
|
||||||
implementation("com.google.accompanist:accompanist-insets:0.30.1")
|
|
||||||
|
|
||||||
// Video support
|
// Video support
|
||||||
implementation("com.google.android.exoplayer:exoplayer:2.19.1")
|
implementation("com.google.android.exoplayer:exoplayer:2.19.1")
|
||||||
|
|||||||
+1
-13
@@ -3,25 +3,13 @@ package chat.simplex.common.platform
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding()
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
actual fun ProvideWindowInsets(
|
|
||||||
consumeWindowInsets: Boolean,
|
|
||||||
windowInsetsAnimationsEnabled: Boolean,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
com.google.accompanist.insets.ProvideWindowInsets(content = content)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun Modifier.desktopOnExternalDrag(
|
actual fun Modifier.desktopOnExternalDrag(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
onFiles: (List<File>) -> Unit,
|
onFiles: (List<File>) -> Unit,
|
||||||
onImage: (Painter) -> Unit,
|
onImage: (File) -> Unit,
|
||||||
onText: (String) -> Unit
|
onText: (String) -> Unit
|
||||||
): Modifier = this
|
): Modifier = this
|
||||||
|
|
||||||
|
|||||||
+16
-5
@@ -6,8 +6,7 @@ import android.graphics.drawable.ColorDrawable
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.OnReceiveContentListener
|
import android.view.*
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.*
|
import android.view.inputmethod.*
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -26,6 +25,7 @@ import androidx.compose.ui.unit.LayoutDirection
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
@@ -94,8 +94,8 @@ actual fun PlatformTextField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||||
AndroidView(modifier = Modifier, factory = {
|
AndroidView(modifier = Modifier, factory = { context ->
|
||||||
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
|
val editText = @SuppressLint("AppCompatCustomView") object: EditText(context) {
|
||||||
override fun setOnReceiveContentListener(
|
override fun setOnReceiveContentListener(
|
||||||
mimeTypes: Array<out String>?,
|
mimeTypes: Array<out String>?,
|
||||||
listener: OnReceiveContentListener?
|
listener: OnReceiveContentListener?
|
||||||
@@ -140,6 +140,13 @@ actual fun PlatformTextField(
|
|||||||
Log.e(TAG, e.stackTraceToString())
|
Log.e(TAG, e.stackTraceToString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||||
|
// shows keyboard when user had search field on ChatView focused before clicking on this text field
|
||||||
|
// it still produce weird animation of closing/opening keyboard but the solution is to replace this Android EditText with Compose BasicTextField
|
||||||
|
if (hasFocus) {
|
||||||
|
showKeyboard = true
|
||||||
|
}
|
||||||
|
}
|
||||||
editText.doOnTextChanged { text, _, _, _ ->
|
editText.doOnTextChanged { text, _, _, _ ->
|
||||||
if (!composeState.value.inProgress) {
|
if (!composeState.value.inProgress) {
|
||||||
onMessageChange(text.toString())
|
onMessageChange(text.toString())
|
||||||
@@ -148,8 +155,12 @@ actual fun PlatformTextField(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
|
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
|
||||||
editText
|
val workaround = WorkaroundFocusSearchLayout(context)
|
||||||
|
workaround.addView(editText)
|
||||||
|
workaround.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
workaround
|
||||||
}) {
|
}) {
|
||||||
|
val it = it.children.first() as EditText
|
||||||
it.setTextColor(textColor.toArgb())
|
it.setTextColor(textColor.toArgb())
|
||||||
it.setHintTextColor(hintColor.toArgb())
|
it.setHintTextColor(hintColor.toArgb())
|
||||||
it.hint = placeholder
|
it.hint = placeholder
|
||||||
|
|||||||
+7
-3
@@ -6,11 +6,11 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.text.BidiFormatter
|
import android.text.BidiFormatter
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -50,7 +50,11 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
actual fun windowWidth(): Dp {
|
||||||
|
val direction = LocalLayoutDirection.current
|
||||||
|
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
|
return LocalConfiguration.current.screenWidthDp.dp - cutout.calculateStartPadding(direction) - cutout.calculateEndPadding(direction)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp
|
actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp
|
||||||
|
|||||||
+129
-30
@@ -7,10 +7,12 @@ import androidx.compose.foundation.lazy.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
|
import chat.simplex.common.views.chatlist.NavigationBarBackground
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
@@ -25,25 +27,74 @@ actual fun LazyColumnWithScrollBar(
|
|||||||
horizontalAlignment: Alignment.Horizontal,
|
horizontalAlignment: Alignment.Horizontal,
|
||||||
flingBehavior: FlingBehavior,
|
flingBehavior: FlingBehavior,
|
||||||
userScrollEnabled: Boolean,
|
userScrollEnabled: Boolean,
|
||||||
|
additionalBarOffset: State<Dp>?,
|
||||||
|
fillMaxSize: Boolean,
|
||||||
content: LazyListScope.() -> Unit
|
content: LazyListScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
val handler = LocalAppBarHandler.current
|
||||||
val connection = LocalAppBarHandler.current?.connection
|
require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" }
|
||||||
|
|
||||||
|
val state = state ?: handler.listState
|
||||||
|
val connection = handler.connection
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { state.firstVisibleItemScrollOffset }
|
if (reverseLayout) {
|
||||||
.filter { state.firstVisibleItemIndex == 0 }
|
snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 }
|
||||||
.collect { scrollPosition ->
|
.collect { scrollPosition ->
|
||||||
val offset = connection?.appBarOffset
|
connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) {
|
||||||
if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
|
state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding
|
||||||
connection.appBarOffset = -scrollPosition.toFloat()
|
} else {
|
||||||
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
|
// show always when last item is not visible
|
||||||
|
-1000f
|
||||||
|
}
|
||||||
|
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
snapshotFlow { state.firstVisibleItemScrollOffset }
|
||||||
|
.filter { state.firstVisibleItemIndex == 0 }
|
||||||
|
.collect { scrollPosition ->
|
||||||
|
val offset = connection.appBarOffset
|
||||||
|
if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) {
|
||||||
|
connection.appBarOffset = -scrollPosition.toFloat()
|
||||||
|
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (connection != null) {
|
LazyColumn(
|
||||||
LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
if (fillMaxSize) {
|
||||||
} else {
|
Modifier.fillMaxSize().copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
|
||||||
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
} else {
|
||||||
|
Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
contentPadding,
|
||||||
|
reverseLayout,
|
||||||
|
verticalArrangement,
|
||||||
|
horizontalAlignment,
|
||||||
|
flingBehavior,
|
||||||
|
userScrollEnabled
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun LazyColumnWithScrollBarNoAppBar(
|
||||||
|
modifier: Modifier,
|
||||||
|
state: LazyListState?,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
reverseLayout: Boolean,
|
||||||
|
verticalArrangement: Arrangement.Vertical,
|
||||||
|
horizontalAlignment: Alignment.Horizontal,
|
||||||
|
flingBehavior: FlingBehavior,
|
||||||
|
userScrollEnabled: Boolean,
|
||||||
|
additionalBarOffset: State<Dp>?,
|
||||||
|
content: LazyListScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val state = state ?: rememberLazyListState()
|
||||||
|
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) {
|
||||||
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,32 +105,80 @@ actual fun ColumnWithScrollBar(
|
|||||||
horizontalAlignment: Alignment.Horizontal,
|
horizontalAlignment: Alignment.Horizontal,
|
||||||
state: ScrollState?,
|
state: ScrollState?,
|
||||||
maxIntrinsicSize: Boolean,
|
maxIntrinsicSize: Boolean,
|
||||||
|
fillMaxSize: Boolean,
|
||||||
content: @Composable() (ColumnScope.() -> Unit)
|
content: @Composable() (ColumnScope.() -> Unit)
|
||||||
) {
|
) {
|
||||||
val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState()
|
val handler = LocalAppBarHandler.current
|
||||||
val connection = LocalAppBarHandler.current?.connection
|
require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" }
|
||||||
|
|
||||||
|
val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier).imePadding() else modifier.imePadding()
|
||||||
|
val state = state ?: handler.scrollState
|
||||||
|
val connection = handler.connection
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { state.value }
|
snapshotFlow { state.value }
|
||||||
.collect { scrollPosition ->
|
.collect { scrollPosition ->
|
||||||
val offset = connection?.appBarOffset
|
val offset = connection.appBarOffset
|
||||||
if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
|
if ((offset + scrollPosition).absoluteValue > 1) {
|
||||||
connection.appBarOffset = -scrollPosition.toFloat()
|
connection.appBarOffset = -scrollPosition.toFloat()
|
||||||
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
|
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (connection != null) {
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
Column(
|
Box(Modifier.fillMaxHeight()) {
|
||||||
if (maxIntrinsicSize) {
|
Column(
|
||||||
modifier.nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max)
|
if (maxIntrinsicSize) {
|
||||||
} else {
|
Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max)
|
||||||
modifier.nestedScroll(connection).verticalScroll(state)
|
} else {
|
||||||
}, verticalArrangement, horizontalAlignment, content)
|
Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state)
|
||||||
} else {
|
}, verticalArrangement, horizontalAlignment
|
||||||
Column(if (maxIntrinsicSize) {
|
) {
|
||||||
|
if (oneHandUI.value) {
|
||||||
|
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars))
|
||||||
|
content()
|
||||||
|
Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier))
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
||||||
|
content()
|
||||||
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!oneHandUI.value) {
|
||||||
|
NavigationBarBackground(false, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun ColumnWithScrollBarNoAppBar(
|
||||||
|
modifier: Modifier,
|
||||||
|
verticalArrangement: Arrangement.Vertical,
|
||||||
|
horizontalAlignment: Alignment.Horizontal,
|
||||||
|
state: ScrollState?,
|
||||||
|
maxIntrinsicSize: Boolean,
|
||||||
|
content: @Composable() (ColumnScope.() -> Unit)
|
||||||
|
) {
|
||||||
|
val modifier = modifier.imePadding()
|
||||||
|
val state = state ?: rememberScrollState()
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
Box(Modifier.fillMaxHeight()) {
|
||||||
|
Column(
|
||||||
|
if (maxIntrinsicSize) {
|
||||||
modifier.verticalScroll(state).height(IntrinsicSize.Max)
|
modifier.verticalScroll(state).height(IntrinsicSize.Max)
|
||||||
} else {
|
} else {
|
||||||
modifier.verticalScroll(state)
|
modifier.verticalScroll(state)
|
||||||
}, verticalArrangement, horizontalAlignment, content)
|
}, verticalArrangement, horizontalAlignment
|
||||||
|
) {
|
||||||
|
if (oneHandUI.value) {
|
||||||
|
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
|
||||||
|
content()
|
||||||
|
} else {
|
||||||
|
content()
|
||||||
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!oneHandUI.value) {
|
||||||
|
NavigationBarBackground(false, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-22
@@ -3,17 +3,18 @@ package chat.simplex.common.platform
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.graphics.Rect
|
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import chat.simplex.common.AppScreen
|
import chat.simplex.common.AppScreen
|
||||||
import chat.simplex.common.model.clear
|
import chat.simplex.common.model.clear
|
||||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import androidx.compose.ui.platform.LocalContext as LocalContext1
|
import androidx.compose.ui.platform.LocalContext as LocalContext1
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -43,28 +44,13 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun getKeyboardState(): State<KeyboardState> {
|
actual fun getKeyboardState(): State<KeyboardState> {
|
||||||
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) }
|
val density = LocalDensity.current
|
||||||
val view = LocalView.current
|
val ime = WindowInsets.ime
|
||||||
DisposableEffect(view) {
|
return remember {
|
||||||
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
|
derivedStateOf {
|
||||||
val rect = Rect()
|
if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened
|
||||||
view.getWindowVisibleDisplayFrame(rect)
|
|
||||||
val screenHeight = view.rootView.height
|
|
||||||
val keypadHeight = screenHeight - rect.bottom
|
|
||||||
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
|
|
||||||
KeyboardState.Opened
|
|
||||||
} else {
|
|
||||||
KeyboardState.Closed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
|
|
||||||
|
|
||||||
onDispose {
|
|
||||||
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyboardState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {
|
actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {
|
||||||
|
|||||||
+14
-9
@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
@@ -48,6 +47,7 @@ import chat.simplex.common.model.ChatController.appPrefs
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import com.google.accompanist.permissions.*
|
import com.google.accompanist.permissions.*
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
@@ -329,11 +329,14 @@ private fun ActiveCallOverlayLayout(
|
|||||||
flipCamera: () -> Unit
|
flipCamera: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
|
CallAppBar(
|
||||||
if (call.hasVideo) {
|
title = {
|
||||||
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
if (call.hasVideo) {
|
||||||
}
|
Text(call.contact.chatViewName, Modifier.offset(x = (-4).dp).padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onBack = { chatModel.activeCallViewIsCollapsed.value = true }
|
||||||
|
)
|
||||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectSoundDevice(size: Dp) {
|
fun SelectSoundDevice(size: Dp) {
|
||||||
@@ -411,6 +414,7 @@ private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Bool
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) {
|
private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) {
|
||||||
|
val ripple = remember { ripple(bounded = false, radius = size / 2, color = background.lighter(0.1f)) }
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.background(background, CircleShape)
|
.background(background, CircleShape)
|
||||||
@@ -419,7 +423,7 @@ private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, backg
|
|||||||
onClick = action,
|
onClick = action,
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)),
|
indication = ripple,
|
||||||
enabled = enabled
|
enabled = enabled
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -590,8 +594,9 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ModalView(background = Color.Black, showClose = false, close = {}) {
|
ModalView(background = Color.Black, showAppBar = false, close = {}) {
|
||||||
ColumnWithScrollBar(Modifier.fillMaxSize()) {
|
Column {
|
||||||
|
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
|
||||||
AppBarTitle(stringResource(MR.strings.permissions_required))
|
AppBarTitle(stringResource(MR.strings.permissions_required))
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
val onClick = {
|
val onClick = {
|
||||||
|
|||||||
-9
@@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
|
|||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import chat.simplex.common.model.CIFile
|
import chat.simplex.common.model.CIFile
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.CurrentColors
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
import chat.simplex.common.views.helpers.ModalManager
|
import chat.simplex.common.views.helpers.ModalManager
|
||||||
@@ -39,14 +38,6 @@ actual fun SimpleAndAnimatedImageView(
|
|||||||
if (getLoadedFilePath(file) != null) {
|
if (getLoadedFilePath(file) != null) {
|
||||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||||
ImageFullScreenView(imageProvider, close)
|
ImageFullScreenView(imageProvider, close)
|
||||||
if (smallView) {
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
val c = CurrentColors.value.colors
|
|
||||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-21
@@ -1,32 +1,26 @@
|
|||||||
package chat.simplex.common.views.chatlist
|
package chat.simplex.common.views.chatlist
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.*
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.simplex.common.ANDROID_CALL_TOP_PADDING
|
import chat.simplex.common.ANDROID_CALL_TOP_PADDING
|
||||||
import chat.simplex.common.model.durationText
|
import chat.simplex.common.model.durationText
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
import chat.simplex.common.views.helpers.*
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
|
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
|
||||||
@@ -38,11 +32,12 @@ private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun ActiveCallInteractiveArea(call: Call) {
|
actual fun ActiveCallInteractiveArea(call: Call) {
|
||||||
val onClick = { platform.androidStartCallActivity(false) }
|
val onClick = { platform.androidStartCallActivity(false) }
|
||||||
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
|
val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
|
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) {
|
||||||
val source = remember { MutableInteractionSource() }
|
val source = remember { MutableInteractionSource() }
|
||||||
val indication = rememberRipple(bounded = true, 3000.dp)
|
val ripple = remember { ripple(bounded = true, 3000.dp) }
|
||||||
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) {
|
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) {
|
||||||
GreenLine(call)
|
GreenLine(statusBar, call)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -50,7 +45,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
|
|||||||
.size(CALL_BOTTOM_ICON_HEIGHT)
|
.size(CALL_BOTTOM_ICON_HEIGHT)
|
||||||
.background(SimplexGreen, CircleShape)
|
.background(SimplexGreen, CircleShape)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.clickable(onClick = onClick, indication = indication, interactionSource = source)
|
.clickable(onClick = onClick, indication = ripple, interactionSource = source)
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
@@ -63,16 +58,13 @@ actual fun ActiveCallInteractiveArea(call: Call) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary solution for storing a color that needs to be applied after call ends
|
|
||||||
var statusBarColorAfterCall = mutableIntStateOf(CurrentColors.value.colors.background.toArgb())
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GreenLine(call: Call) {
|
private fun GreenLine(statusBarHeight: Dp, call: Call) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(SimplexGreen)
|
.background(SimplexGreen)
|
||||||
.padding(top = -CALL_TOP_OFFSET)
|
.padding(top = -CALL_TOP_OFFSET + statusBarHeight)
|
||||||
.padding(horizontal = DEFAULT_PADDING),
|
.padding(horizontal = DEFAULT_PADDING),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
@@ -81,12 +73,10 @@ private fun GreenLine(call: Call) {
|
|||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
CallDuration(call)
|
CallDuration(call)
|
||||||
}
|
}
|
||||||
val window = (LocalContext.current as Activity).window
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
statusBarColorAfterCall.intValue = window.statusBarColor
|
platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight)
|
||||||
window.statusBarColor = SimplexGreen.toArgb()
|
|
||||||
onDispose {
|
onDispose {
|
||||||
window.statusBarColor = statusBarColorAfterCall.intValue
|
platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-74
@@ -19,13 +19,11 @@ import androidx.compose.ui.layout.onSizeChanged
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
|
||||||
import chat.simplex.common.model.User
|
import chat.simplex.common.model.User
|
||||||
import chat.simplex.common.model.UserInfo
|
import chat.simplex.common.model.UserInfo
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -36,6 +34,7 @@ private val USER_PICKER_ROW_PADDING = 16.dp
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun UserPickerUsersSection(
|
actual fun UserPickerUsersSection(
|
||||||
users: List<UserInfo>,
|
users: List<UserInfo>,
|
||||||
|
iconColor: Color,
|
||||||
stopped: Boolean,
|
stopped: Boolean,
|
||||||
onUserClicked: (user: User) -> Unit,
|
onUserClicked: (user: User) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -140,87 +139,73 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
|
|||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
Box(
|
Box {
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.then(clickableModifier)
|
|
||||||
.drawBehind {
|
|
||||||
val pos = when {
|
|
||||||
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
|
|
||||||
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
|
|
||||||
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
|
|
||||||
else -> 1 - dismissState.progress.fraction
|
|
||||||
}
|
|
||||||
val colors = CurrentColors.value.colors
|
|
||||||
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
|
|
||||||
val adjustedAlpha = resultingColor.alpha * calculateFraction(pos = pos)
|
|
||||||
val shadingColor = resultingColor.copy(alpha = adjustedAlpha)
|
|
||||||
|
|
||||||
if (pickerState.value.isVisible()) {
|
|
||||||
platform.androidSetDrawerStatusAndNavBarColor(
|
|
||||||
isLight = colors.isLight,
|
|
||||||
drawerShadingColor = shadingColor,
|
|
||||||
toolbarOnTop = !appPrefs.oneHandUI.get(),
|
|
||||||
navBarColor = colors.background.mixWith(colors.onBackground, 1 - userPickerAlpha())
|
|
||||||
)
|
|
||||||
} else if (ModalManager.start.modalCount.value == 0) {
|
|
||||||
platform.androidSetDrawerStatusAndNavBarColor(
|
|
||||||
isLight = colors.isLight,
|
|
||||||
drawerShadingColor = shadingColor,
|
|
||||||
toolbarOnTop = !appPrefs.oneHandUI.get(),
|
|
||||||
navBarColor = (if (appPrefs.oneHandUI.get() && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
|
||||||
colors.background.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
|
|
||||||
} else {
|
|
||||||
colors.background
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
drawRect(
|
|
||||||
if (pos != 0f) resultingColor else Color.Transparent,
|
|
||||||
alpha = calculateFraction(pos = pos)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.graphicsLayer {
|
|
||||||
if (heightValue == 0) {
|
|
||||||
alpha = 0f
|
|
||||||
}
|
|
||||||
translationY = dismissState.offset.value
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.BottomCenter
|
|
||||||
) {
|
|
||||||
Box(
|
Box(
|
||||||
Modifier.onSizeChanged { height.intValue = it.height }
|
Modifier
|
||||||
) {
|
.fillMaxSize()
|
||||||
KeyChangeEffect(pickerIsVisible) {
|
.then(clickableModifier)
|
||||||
if (pickerState.value.isVisible()) {
|
.drawBehind {
|
||||||
try {
|
val pos = calculatePosition(dismissState)
|
||||||
dismissState.animateTo(DismissValue.Default, userPickerAnimSpec())
|
val colors = CurrentColors.value.colors
|
||||||
} catch (e: CancellationException) {
|
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
|
||||||
Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}")
|
drawRect(
|
||||||
pickerState.value = AnimatedViewState.GONE
|
if (pos != 0f) resultingColor else Color.Transparent,
|
||||||
|
alpha = calculateFraction(pos = pos)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.graphicsLayer {
|
||||||
|
if (heightValue == 0) {
|
||||||
|
alpha = 0f
|
||||||
}
|
}
|
||||||
} else {
|
translationY = dismissState.offset.value
|
||||||
try {
|
},
|
||||||
dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec())
|
contentAlignment = Alignment.BottomCenter
|
||||||
} catch (e: CancellationException) {
|
) {
|
||||||
Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}")
|
Box(
|
||||||
pickerState.value = AnimatedViewState.VISIBLE
|
Modifier.onSizeChanged { height.intValue = it.height }
|
||||||
|
) {
|
||||||
|
KeyChangeEffect(pickerIsVisible) {
|
||||||
|
if (pickerState.value.isVisible()) {
|
||||||
|
try {
|
||||||
|
dismissState.animateTo(DismissValue.Default, userPickerAnimSpec())
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}")
|
||||||
|
pickerState.value = AnimatedViewState.GONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec())
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}")
|
||||||
|
pickerState.value = AnimatedViewState.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
val draggableModifier = if (height.intValue != 0)
|
||||||
val draggableModifier = if (height.intValue != 0)
|
Modifier.draggableBottomDrawerModifier(
|
||||||
Modifier.draggableBottomDrawerModifier(
|
state = dismissState,
|
||||||
state = dismissState,
|
swipeDistance = height.intValue.toFloat(),
|
||||||
swipeDistance = height.intValue.toFloat(),
|
)
|
||||||
)
|
else Modifier
|
||||||
else Modifier
|
Box(draggableModifier.then(modifier).navigationBarsPadding()) {
|
||||||
Box(draggableModifier.then(modifier)) {
|
content()
|
||||||
content()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NavigationBarBackground(
|
||||||
|
modifier = Modifier.graphicsLayer { alpha = if (calculatePosition(dismissState) > 0.1f) 1f else 0f },
|
||||||
|
color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun calculatePosition(dismissState: DismissState): Float = when {
|
||||||
|
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
|
||||||
|
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
|
||||||
|
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
|
||||||
|
else -> 1 - dismissState.progress.fraction
|
||||||
|
}
|
||||||
|
|
||||||
private fun Modifier.draggableBottomDrawerModifier(
|
private fun Modifier.draggableBottomDrawerModifier(
|
||||||
state: DismissState,
|
state: DismissState,
|
||||||
swipeDistance: Float,
|
swipeDistance: Float,
|
||||||
|
|||||||
+2
@@ -171,6 +171,8 @@ actual fun GetImageBottomSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
|
.imePadding()
|
||||||
|
.navigationBarsPadding()
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
if (!focusState.hasFocus) hideBottomSheet()
|
if (!focusState.hasFocus) hideBottomSheet()
|
||||||
}
|
}
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A workaround for the ANR issue on Compose 1.7.x.
|
||||||
|
* https://issuetracker.google.com/issues/369354336
|
||||||
|
* Code from:
|
||||||
|
* https://issuetracker.google.com/issues/369354336#comment8
|
||||||
|
*/
|
||||||
|
class WorkaroundFocusSearchLayout : FrameLayout {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
) : super(context)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet?,
|
||||||
|
) : super(context, attrs)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet?,
|
||||||
|
defStyleAttr: Int,
|
||||||
|
) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet?,
|
||||||
|
defStyleAttr: Int,
|
||||||
|
defStyleRes: Int,
|
||||||
|
) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||||
|
|
||||||
|
override fun focusSearch(focused: View?, direction: Int): View? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-7
@@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings
|
|||||||
|
|
||||||
import SectionBottomSpacer
|
import SectionBottomSpacer
|
||||||
import SectionDividerSpaced
|
import SectionDividerSpaced
|
||||||
|
import SectionSpacer
|
||||||
import SectionView
|
import SectionView
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
@@ -31,6 +32,7 @@ import chat.simplex.common.model.ChatModel
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.helpers.APPLICATION_ID
|
import chat.simplex.common.helpers.APPLICATION_ID
|
||||||
import chat.simplex.common.helpers.saveAppLocale
|
import chat.simplex.common.helpers.saveAppLocale
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
@@ -75,9 +77,7 @@ fun AppearanceScope.AppearanceLayout(
|
|||||||
systemDarkTheme: SharedPreference<String?>,
|
systemDarkTheme: SharedPreference<String?>,
|
||||||
changeIcon: (AppIcon) -> Unit,
|
changeIcon: (AppIcon) -> Unit,
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
||||||
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
|
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -106,15 +106,15 @@ fun AppearanceScope.AppearanceLayout(
|
|||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
|
|
||||||
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) {
|
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
|
||||||
val c = CurrentColors.value.colors
|
|
||||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
ThemesSection(systemDarkTheme)
|
ThemesSection(systemDarkTheme)
|
||||||
|
|
||||||
|
SectionDividerSpaced()
|
||||||
|
AppToolbarsSection()
|
||||||
|
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
MessageShapeSection()
|
MessageShapeSection()
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -13,14 +13,13 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun SettingsSectionApp(
|
actual fun SettingsSectionApp(
|
||||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
|
||||||
showVersion: () -> Unit,
|
showVersion: () -> Unit,
|
||||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||||
) {
|
) {
|
||||||
SectionView(stringResource(MR.strings.settings_section_title_app)) {
|
SectionView(stringResource(MR.strings.settings_section_title_app)) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp)
|
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) })
|
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) })
|
||||||
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
|
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) })
|
||||||
AppVersionItem(showVersion)
|
AppVersionItem(showVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.*
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
|
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
@@ -39,14 +42,39 @@ import dev.icerock.moko.resources.compose.painterResource
|
|||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScreen() {
|
fun AppScreen() {
|
||||||
AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() }
|
AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() }
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
// This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area
|
||||||
MainScreen()
|
// (because nav bar and holes located on vertical sides of screen in landscape view)
|
||||||
|
val direction = LocalLayoutDirection.current
|
||||||
|
val safePadding = WindowInsets.safeDrawing.asPaddingValues()
|
||||||
|
val cutout = WindowInsets.displayCutout.asPaddingValues()
|
||||||
|
val cutoutStart = cutout.calculateStartPadding(direction)
|
||||||
|
val cutoutEnd = cutout.calculateEndPadding(direction)
|
||||||
|
val cutoutMax = maxOf(cutoutStart, cutoutEnd)
|
||||||
|
val paddingStartUntouched = safePadding.calculateStartPadding(direction)
|
||||||
|
val paddingStart = paddingStartUntouched - cutoutStart
|
||||||
|
val paddingEndUntouched = safePadding.calculateEndPadding(direction)
|
||||||
|
val paddingEnd = paddingEndUntouched - cutoutEnd
|
||||||
|
// Such a strange layout is needed because the main content should be covered by solid color in order to hide overflow
|
||||||
|
// of some elements that may have negative offset (so, can't use Row {}).
|
||||||
|
// To check: go to developer settings of Android, choose Display cutout -> Punch hole, and rotate the phone to landscape, open any chat
|
||||||
|
Box {
|
||||||
|
val fullscreenGallery = remember { chatModel.fullscreenGalleryVisible }
|
||||||
|
Box(Modifier.padding(start = paddingStart + cutoutMax, end = paddingEnd + cutoutMax).consumeWindowInsets(PaddingValues(start = paddingStartUntouched, end = paddingEndUntouched))) {
|
||||||
|
Box(Modifier.drawBehind {
|
||||||
|
if (fullscreenGallery.value) {
|
||||||
|
drawRect(Color.Black, topLeft = Offset(-(paddingStart + cutoutMax).toPx(), 0f), Size(size.width + (paddingStart + cutoutMax).toPx() + (paddingEnd + cutoutMax).toPx(), size.height))
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
MainScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +166,9 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
SetupClipboardListener()
|
SetupClipboardListener()
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
AndroidScreen(userPickerState)
|
AndroidWrapInCallLayout {
|
||||||
|
AndroidScreen(userPickerState)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
DesktopScreen(userPickerState)
|
DesktopScreen(userPickerState)
|
||||||
}
|
}
|
||||||
@@ -170,7 +200,9 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
ModalManager.fullscreen.showInView()
|
AndroidWrapInCallLayout {
|
||||||
|
ModalManager.fullscreen.showInView()
|
||||||
|
}
|
||||||
SwitchingUsersView()
|
SwitchingUsersView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,19 +269,39 @@ fun MainScreen() {
|
|||||||
|
|
||||||
val ANDROID_CALL_TOP_PADDING = 40.dp
|
val ANDROID_CALL_TOP_PADDING = 40.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AndroidWrapInCallLayout(content: @Composable () -> Unit) {
|
||||||
|
val call = remember { chatModel.activeCall}.value
|
||||||
|
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
|
||||||
|
Box {
|
||||||
|
Box(Modifier.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
if (call != null && showCallArea) {
|
||||||
|
ActiveCallInteractiveArea(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
val call = remember { chatModel.activeCall} .value
|
|
||||||
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
|
|
||||||
val currentChatId = remember { mutableStateOf(chatModel.chatId.value) }
|
val currentChatId = remember { mutableStateOf(chatModel.chatId.value) }
|
||||||
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
||||||
|
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
|
val direction = LocalLayoutDirection.current
|
||||||
|
val hasCutout = cutout.calculateStartPadding(direction) + cutout.calculateEndPadding(direction) > 0.dp
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
// clipping only for devices with cutout currently visible on sides. It prevents showing chat list with open chat view
|
||||||
|
// In order cases it's not needed to use clip
|
||||||
|
.then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
translationX = -offset.value.dp.toPx()
|
// minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape
|
||||||
|
// because in this case (at least in emulator) maxWidth changes in two steps: big first, smaller on next frame.
|
||||||
|
// But offset is remembered already, so this is a better way than dropping a value of offset
|
||||||
|
translationX = -minOf(offset.value.dp, maxWidth).toPx()
|
||||||
}
|
}
|
||||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
|
||||||
) {
|
) {
|
||||||
StartPartOfScreen(userPickerState)
|
StartPartOfScreen(userPickerState)
|
||||||
}
|
}
|
||||||
@@ -271,51 +323,40 @@ fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
|||||||
snapshotFlow { chatModel.chatId.value }
|
snapshotFlow { chatModel.chatId.value }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect {
|
.collect {
|
||||||
if (it == null) {
|
if (it == null) onComposed(null)
|
||||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
|
||||||
onComposed(null)
|
|
||||||
}
|
|
||||||
currentChatId.value = it
|
currentChatId.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
snapshotFlow { ModalManager.center.modalCount.value > 0 }
|
|
||||||
.filter { chatModel.chatId.value == null }
|
|
||||||
.collect { modalBackground ->
|
|
||||||
if (chatModel.newChatSheetVisible.value) {
|
|
||||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get())
|
|
||||||
} else if (modalBackground) {
|
|
||||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
|
|
||||||
} else {
|
|
||||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(Modifier
|
Box(Modifier
|
||||||
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
|
.then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
|
||||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
.graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() }
|
||||||
) Box2@{
|
) Box2@{
|
||||||
currentChatId.value?.let {
|
currentChatId.value?.let {
|
||||||
ChatView(currentChatId, onComposed)
|
ChatView(currentChatId, onComposed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (call != null && showCallArea) {
|
|
||||||
ActiveCallInteractiveArea(call)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||||
if (chatModel.setDeliveryReceipts.value) {
|
if (chatModel.setDeliveryReceipts.value) {
|
||||||
SetDeliveryReceiptsView(chatModel)
|
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||||
|
SetDeliveryReceiptsView(chatModel)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val stopped = chatModel.chatRunning.value == false
|
val stopped = chatModel.chatRunning.value == false
|
||||||
if (chatModel.sharedContent.value == null)
|
if (chatModel.sharedContent.value == null) {
|
||||||
ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped)
|
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||||
else
|
ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped)
|
||||||
ShareListView(chatModel, stopped)
|
}
|
||||||
|
} else {
|
||||||
|
// LALAL initial load of view doesn't show blur. Focusing text field shows it
|
||||||
|
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) {
|
||||||
|
ShareListView(chatModel, stopped)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ object ChatModel {
|
|||||||
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
|
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
|
||||||
val newChatSheetVisible = mutableStateOf(false)
|
val newChatSheetVisible = mutableStateOf(false)
|
||||||
|
|
||||||
|
// Needed to apply black color to left/right cutout area on Android
|
||||||
|
val fullscreenGalleryVisible = mutableStateOf(false)
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
val notificationPreviewMode by lazy {
|
val notificationPreviewMode by lazy {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
|
|||||||
+19
-1
@@ -118,6 +118,9 @@ class AppPreferences {
|
|||||||
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
||||||
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
|
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
|
||||||
val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0)
|
val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0)
|
||||||
|
// Blur broken on Android 12, see https://github.com/chrisbanes/haze/issues/77. And not available before 12
|
||||||
|
val deviceSupportsBlur = appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 32
|
||||||
|
val appearanceBarsBlurRadius = mkIntPreference(SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS, if (deviceSupportsBlur) 50 else 0)
|
||||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||||
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
||||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||||
@@ -223,6 +226,8 @@ class AppPreferences {
|
|||||||
val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true)
|
val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true)
|
||||||
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
|
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
|
||||||
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
|
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
|
||||||
|
val inAppBarsDefaultAlpha = if (deviceSupportsBlur) 0.875f else 0.975f
|
||||||
|
val inAppBarsAlpha = mkFloatPreference(SHARED_PREFS_IN_APP_BARS_ALPHA, inAppBarsDefaultAlpha)
|
||||||
|
|
||||||
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
||||||
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
|
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
|
||||||
@@ -244,7 +249,7 @@ class AppPreferences {
|
|||||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||||
|
|
||||||
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid)
|
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true)
|
||||||
|
|
||||||
val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf(
|
val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf(
|
||||||
laNoticeShown to false,
|
laNoticeShown to false,
|
||||||
@@ -362,6 +367,7 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
||||||
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
|
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
|
||||||
private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius"
|
private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius"
|
||||||
|
private const val SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS = "AppearanceBarsBlurRadius"
|
||||||
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
||||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||||
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
|
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
|
||||||
@@ -428,6 +434,7 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail"
|
private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail"
|
||||||
private const val SHARED_PREFS_FONT_SCALE = "FontScale"
|
private const val SHARED_PREFS_FONT_SCALE = "FontScale"
|
||||||
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
|
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
|
||||||
|
private const val SHARED_PREFS_IN_APP_BARS_ALPHA = "InAppBarsAlpha"
|
||||||
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
||||||
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
||||||
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
|
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
|
||||||
@@ -1226,6 +1233,15 @@ object ChatController {
|
|||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
||||||
|
&& r.chatError.agentError is AgentErrorType.SMP
|
||||||
|
&& r.chatError.agentError.smpErr is SMPErrorType.QUOTA -> {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.connection_error_quota),
|
||||||
|
generalGetString(MR.strings.connection_error_quota_desc)
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (!(networkErrorAlert(r))) {
|
if (!(networkErrorAlert(r))) {
|
||||||
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
|
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
|
||||||
@@ -6045,6 +6061,7 @@ sealed class SMPErrorType {
|
|||||||
is AUTH -> "AUTH"
|
is AUTH -> "AUTH"
|
||||||
is CRYPTO -> "CRYPTO"
|
is CRYPTO -> "CRYPTO"
|
||||||
is QUOTA -> "QUOTA"
|
is QUOTA -> "QUOTA"
|
||||||
|
is STORE -> "STORE ${storeErr}"
|
||||||
is NO_MSG -> "NO_MSG"
|
is NO_MSG -> "NO_MSG"
|
||||||
is LARGE_MSG -> "LARGE_MSG"
|
is LARGE_MSG -> "LARGE_MSG"
|
||||||
is EXPIRED -> "EXPIRED"
|
is EXPIRED -> "EXPIRED"
|
||||||
@@ -6057,6 +6074,7 @@ sealed class SMPErrorType {
|
|||||||
@Serializable @SerialName("AUTH") class AUTH: SMPErrorType()
|
@Serializable @SerialName("AUTH") class AUTH: SMPErrorType()
|
||||||
@Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType()
|
@Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType()
|
||||||
@Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType()
|
@Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType()
|
||||||
|
@Serializable @SerialName("STORE") class STORE(val storeErr: String): SMPErrorType()
|
||||||
@Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType()
|
@Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType()
|
||||||
@Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType()
|
@Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType()
|
||||||
@Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType()
|
@Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType()
|
||||||
|
|||||||
+1
-10
@@ -14,20 +14,11 @@ import kotlinx.coroutines.*
|
|||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
expect fun Modifier.navigationBarsWithImePadding(): Modifier
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
expect fun ProvideWindowInsets(
|
|
||||||
consumeWindowInsets: Boolean = true,
|
|
||||||
windowInsetsAnimationsEnabled: Boolean = true,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun Modifier.desktopOnExternalDrag(
|
expect fun Modifier.desktopOnExternalDrag(
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
onFiles: (List<File>) -> Unit = {},
|
onFiles: (List<File>) -> Unit = {},
|
||||||
onImage: (Painter) -> Unit = {},
|
onImage: (File) -> Unit = {},
|
||||||
onText: (String) -> Unit = {}
|
onText: (String) -> Unit = {}
|
||||||
): Modifier
|
): Modifier
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import chat.simplex.common.model.ChatId
|
import chat.simplex.common.model.ChatId
|
||||||
import chat.simplex.common.model.NotificationsMode
|
import chat.simplex.common.model.NotificationsMode
|
||||||
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
||||||
interface PlatformInterface {
|
interface PlatformInterface {
|
||||||
@@ -20,12 +21,12 @@ interface PlatformInterface {
|
|||||||
fun androidChatInitializedAndStarted() {}
|
fun androidChatInitializedAndStarted() {}
|
||||||
fun androidIsBackgroundCallAllowed(): Boolean = true
|
fun androidIsBackgroundCallAllowed(): Boolean = true
|
||||||
fun androidSetNightModeIfSupported() {}
|
fun androidSetNightModeIfSupported() {}
|
||||||
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {}
|
fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {}
|
||||||
fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Color, toolbarOnTop: Boolean, navBarColor: Color) {}
|
|
||||||
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
||||||
fun androidPictureInPictureAllowed(): Boolean = true
|
fun androidPictureInPictureAllowed(): Boolean = true
|
||||||
fun androidCallEnded() {}
|
fun androidCallEnded() {}
|
||||||
fun androidRestartNetworkObserver() {}
|
fun androidRestartNetworkObserver() {}
|
||||||
|
val androidApiLevel: Int? get() = null
|
||||||
@Composable fun androidLockPortraitOrientation() {}
|
@Composable fun androidLockPortraitOrientation() {}
|
||||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||||
@Composable fun desktopShowAppUpdateNotice() {}
|
@Composable fun desktopShowAppUpdateNotice() {}
|
||||||
|
|||||||
+34
@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -21,11 +22,44 @@ expect fun LazyColumnWithScrollBar(
|
|||||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
userScrollEnabled: Boolean = true,
|
userScrollEnabled: Boolean = true,
|
||||||
|
additionalBarOffset: State<Dp>? = null,
|
||||||
|
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
|
||||||
|
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
|
||||||
|
fillMaxSize: Boolean = true,
|
||||||
|
content: LazyListScope.() -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun LazyColumnWithScrollBarNoAppBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: LazyListState? = null,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
verticalArrangement: Arrangement.Vertical =
|
||||||
|
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
|
userScrollEnabled: Boolean = true,
|
||||||
|
additionalBarOffset: State<Dp>? = null,
|
||||||
content: LazyListScope.() -> Unit
|
content: LazyListScope.() -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun ColumnWithScrollBar(
|
expect fun ColumnWithScrollBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
state: ScrollState? = null,
|
||||||
|
// set true when you want to show something in the center with respected .fillMaxSize()
|
||||||
|
maxIntrinsicSize: Boolean = false,
|
||||||
|
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
|
||||||
|
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
|
||||||
|
fillMaxSize: Boolean = true,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun ColumnWithScrollBarNoAppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
|||||||
+21
-15
@@ -1,14 +1,14 @@
|
|||||||
package chat.simplex.common.ui.theme
|
package chat.simplex.common.ui.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.*
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.model.ChatController
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
@@ -587,21 +587,27 @@ data class ThemeModeOverride (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier {
|
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier {
|
||||||
return if (baseTheme == DefaultTheme.SIMPLEX) {
|
return drawBehind {
|
||||||
this.background(brush = Brush.linearGradient(
|
copyBackgroundToAppBar(bgLayerSize, bgLayer) {
|
||||||
listOf(
|
if (baseTheme == DefaultTheme.SIMPLEX) {
|
||||||
CurrentColors.value.colors.background.darker(0.4f),
|
drawRect(brush = themedBackgroundBrush())
|
||||||
CurrentColors.value.colors.background.lighter(0.4f)
|
} else {
|
||||||
),
|
drawRect(CurrentColors.value.colors.background)
|
||||||
Offset(0f, Float.POSITIVE_INFINITY),
|
}
|
||||||
Offset(Float.POSITIVE_INFINITY, 0f)
|
}
|
||||||
), shape = shape)
|
|
||||||
} else {
|
|
||||||
this.background(color = CurrentColors.value.colors.background, shape = shape)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun themedBackgroundBrush(): Brush = Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
CurrentColors.value.colors.background.darker(0.4f),
|
||||||
|
CurrentColors.value.colors.background.lighter(0.4f)
|
||||||
|
),
|
||||||
|
Offset(0f, Float.POSITIVE_INFINITY),
|
||||||
|
Offset(Float.POSITIVE_INFINITY, 0f)
|
||||||
|
)
|
||||||
|
|
||||||
val DEFAULT_PADDING = 20.dp
|
val DEFAULT_PADDING = 20.dp
|
||||||
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
||||||
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||||
|
|||||||
+1
-6
@@ -1,7 +1,6 @@
|
|||||||
package chat.simplex.common.ui.theme
|
package chat.simplex.common.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material.Colors
|
import androidx.compose.material.Colors
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
@@ -107,7 +106,7 @@ object ThemeManager {
|
|||||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
platform.androidSetNightModeIfSupported()
|
platform.androidSetNightModeIfSupported()
|
||||||
val c = CurrentColors.value.colors
|
val c = CurrentColors.value.colors
|
||||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get())
|
platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeDarkTheme(theme: String) {
|
fun changeDarkTheme(theme: String) {
|
||||||
@@ -125,10 +124,6 @@ object ThemeManager {
|
|||||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||||
appPrefs.currentThemeIds.set(themeIds)
|
appPrefs.currentThemeIds.set(themeIds)
|
||||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
if (name == ThemeColor.BACKGROUND) {
|
|
||||||
val c = CurrentColors.value.colors
|
|
||||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
|
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
|
||||||
|
|||||||
+74
-73
@@ -7,40 +7,34 @@ import androidx.compose.foundation.lazy.*
|
|||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.layoutId
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.*
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.chat.*
|
import chat.simplex.common.views.chat.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import kotlinx.coroutines.flow.collect
|
import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID
|
||||||
|
import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout
|
||||||
|
import chat.simplex.common.views.chatlist.NavigationBarBackground
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TerminalView(floating: Boolean = false, close: () -> Unit) {
|
fun TerminalView(floating: Boolean = false) {
|
||||||
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
||||||
val close = {
|
|
||||||
close()
|
|
||||||
if (appPlatform.isDesktop) {
|
|
||||||
ModalManager.center.closeModals()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BackHandler(onBack = {
|
|
||||||
close()
|
|
||||||
})
|
|
||||||
TerminalLayout(
|
TerminalLayout(
|
||||||
composeState,
|
composeState,
|
||||||
floating,
|
floating,
|
||||||
sendCommand = { sendCommand(chatModel, composeState) },
|
sendCommand = { sendCommand(chatModel, composeState) },
|
||||||
close
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +63,6 @@ fun TerminalLayout(
|
|||||||
composeState: MutableState<ComposeState>,
|
composeState: MutableState<ComposeState>,
|
||||||
floating: Boolean,
|
floating: Boolean,
|
||||||
sendCommand: () -> Unit,
|
sendCommand: () -> Unit,
|
||||||
close: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||||
val textStyle = remember { mutableStateOf(smallFont) }
|
val textStyle = remember { mutableStateOf(smallFont) }
|
||||||
@@ -77,65 +70,63 @@ fun TerminalLayout(
|
|||||||
fun onMessageChange(s: String) {
|
fun onMessageChange(s: String) {
|
||||||
composeState.value = composeState.value.copy(message = s)
|
composeState.value = composeState.value.copy(message = s)
|
||||||
}
|
}
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Scaffold(
|
val composeViewHeight = remember { mutableStateOf(0.dp) }
|
||||||
topBar = { CloseSheetBar(close) },
|
AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) {
|
||||||
bottomBar = {
|
TerminalLog(floating, composeViewHeight)
|
||||||
Column {
|
Column(
|
||||||
Divider()
|
Modifier
|
||||||
Box(Modifier.padding(horizontal = 8.dp)) {
|
.layoutId(CONSOLE_COMPOSE_LAYOUT_ID)
|
||||||
SendMsgView(
|
.align(Alignment.BottomCenter)
|
||||||
composeState = composeState,
|
.navigationBarsPadding()
|
||||||
showVoiceRecordIcon = false,
|
.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp))
|
||||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
.imePadding()
|
||||||
isDirectChat = false,
|
.padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)
|
||||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
.background(MaterialTheme.colors.background)
|
||||||
sendMsgEnabled = true,
|
|
||||||
sendButtonEnabled = true,
|
|
||||||
nextSendGrpInv = false,
|
|
||||||
needToAllowVoiceToContact = false,
|
|
||||||
allowedVoiceByPrefs = false,
|
|
||||||
userIsObserver = false,
|
|
||||||
userCanSend = true,
|
|
||||||
allowVoiceToContact = {},
|
|
||||||
placeholder = "",
|
|
||||||
sendMessage = { sendCommand() },
|
|
||||||
sendLiveMessage = null,
|
|
||||||
updateLiveMessage = null,
|
|
||||||
editPrevMessage = {},
|
|
||||||
onMessageChange = ::onMessageChange,
|
|
||||||
onFilesPasted = {},
|
|
||||||
textStyle = textStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentColor = LocalContentColor.current,
|
|
||||||
modifier = Modifier.navigationBarsWithImePadding()
|
|
||||||
) { contentPadding ->
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(contentPadding)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
color = MaterialTheme.colors.background,
|
|
||||||
contentColor = LocalContentColor.current
|
|
||||||
) {
|
) {
|
||||||
TerminalLog(floating)
|
Divider()
|
||||||
|
Box(Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
SendMsgView(
|
||||||
|
composeState = composeState,
|
||||||
|
showVoiceRecordIcon = false,
|
||||||
|
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||||
|
isDirectChat = false,
|
||||||
|
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||||
|
sendMsgEnabled = true,
|
||||||
|
sendButtonEnabled = true,
|
||||||
|
nextSendGrpInv = false,
|
||||||
|
needToAllowVoiceToContact = false,
|
||||||
|
allowedVoiceByPrefs = false,
|
||||||
|
userIsObserver = false,
|
||||||
|
userCanSend = true,
|
||||||
|
allowVoiceToContact = {},
|
||||||
|
placeholder = "",
|
||||||
|
sendMessage = { sendCommand() },
|
||||||
|
sendLiveMessage = null,
|
||||||
|
updateLiveMessage = null,
|
||||||
|
editPrevMessage = {},
|
||||||
|
onMessageChange = ::onMessageChange,
|
||||||
|
onFilesPasted = {},
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!oneHandUI.value) {
|
||||||
|
NavigationBarBackground(true, oneHandUI.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TerminalLog(floating: Boolean) {
|
fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
|
||||||
val reversedTerminalItems by remember {
|
val reversedTerminalItems by remember {
|
||||||
derivedStateOf { chatModel.terminalItems.value.asReversed() }
|
derivedStateOf { chatModel.terminalItems.value.asReversed() }
|
||||||
}
|
}
|
||||||
val clipboard = LocalClipboardManager.current
|
|
||||||
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
var autoScrollToBottom = true
|
var autoScrollToBottom = listState.firstVisibleItemIndex <= 1
|
||||||
launch {
|
launch {
|
||||||
snapshotFlow { listState.layoutInfo.totalItemsCount }
|
snapshotFlow { listState.layoutInfo.totalItemsCount }
|
||||||
.filter { autoScrollToBottom }
|
.filter { autoScrollToBottom }
|
||||||
@@ -150,12 +141,21 @@ fun TerminalLog(floating: Boolean) {
|
|||||||
launch {
|
launch {
|
||||||
snapshotFlow { listState.firstVisibleItemIndex }
|
snapshotFlow { listState.firstVisibleItemIndex }
|
||||||
.collect {
|
.collect {
|
||||||
autoScrollToBottom = listState.firstVisibleItemIndex == 0
|
autoScrollToBottom = it == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LazyColumnWithScrollBar(reverseLayout = true, state = listState) {
|
LazyColumnWithScrollBar (
|
||||||
|
reverseLayout = true,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = topPaddingToContent(),
|
||||||
|
bottom = composeViewHeight.value
|
||||||
|
),
|
||||||
|
state = listState,
|
||||||
|
additionalBarOffset = composeViewHeight
|
||||||
|
) {
|
||||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
val rhId = item.remoteHostId
|
val rhId = item.remoteHostId
|
||||||
val rhIdStr = if (rhId == null) "" else "$rhId "
|
val rhIdStr = if (rhId == null) "" else "$rhId "
|
||||||
Text(
|
Text(
|
||||||
@@ -172,13 +172,15 @@ fun TerminalLog(floating: Boolean) {
|
|||||||
ModalManager.start
|
ModalManager.start
|
||||||
}
|
}
|
||||||
modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
|
modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
|
||||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
ColumnWithScrollBar {
|
||||||
val details = item.details
|
SelectionContainer {
|
||||||
.let {
|
val details = item.details
|
||||||
if (it.length < 100_000) it
|
.let {
|
||||||
else it.substring(0, 100_000)
|
if (it.length < 100_000) it
|
||||||
}
|
else it.substring(0, 100_000)
|
||||||
Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
}
|
||||||
|
Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.padding(horizontal = 8.dp, vertical = 4.dp)
|
}.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
@@ -208,8 +210,7 @@ fun PreviewTerminalLayout() {
|
|||||||
TerminalLayout(
|
TerminalLayout(
|
||||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
|
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
|
||||||
sendCommand = {},
|
sendCommand = {},
|
||||||
floating = false,
|
floating = false
|
||||||
close = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-59
@@ -40,8 +40,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
|||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val keyboardState by getKeyboardState()
|
val keyboardState by getKeyboardState()
|
||||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||||
|
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -50,11 +48,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
|||||||
val displayName = rememberSaveable { mutableStateOf("") }
|
val displayName = rememberSaveable { mutableStateOf("") }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING)
|
AppBarTitle(stringResource(MR.strings.create_profile), withPadding = false, bottomPadding = DEFAULT_PADDING)
|
||||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(MR.strings.display_name),
|
stringResource(MR.strings.display_name),
|
||||||
@@ -102,7 +98,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -111,59 +106,42 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
|
|||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val keyboardState by getKeyboardState()
|
val keyboardState by getKeyboardState()
|
||||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||||
val handler = remember { AppBarHandler() }
|
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||||
CompositionLocalProvider(
|
ModalView({
|
||||||
LocalAppBarHandler provides handler
|
if (chatModel.users.none { !it.user.hidden }) {
|
||||||
) {
|
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
} else {
|
||||||
Column(
|
close()
|
||||||
modifier = Modifier
|
}
|
||||||
.fillMaxSize()
|
}) {
|
||||||
.themedBackground(),
|
ColumnWithScrollBar {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
val displayName = rememberSaveable { mutableStateOf("") }
|
||||||
) {
|
val focusRequester = remember { FocusRequester() }
|
||||||
CloseSheetBar(close = {
|
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
|
||||||
if (chatModel.users.none { !it.user.hidden }) {
|
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false)
|
||||||
} else {
|
|
||||||
close()
|
|
||||||
}
|
}
|
||||||
})
|
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
|
||||||
BackHandler(onBack = {
|
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||||
})
|
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||||
|
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
OnboardingActionButton(
|
||||||
|
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||||
|
labelId = MR.strings.create_profile_button,
|
||||||
|
onboarding = null,
|
||||||
|
enabled = canCreateProfile(displayName.value),
|
||||||
|
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
|
||||||
|
)
|
||||||
|
// Reserve space
|
||||||
|
TextButtonBelowOnboardingButton("", null)
|
||||||
|
}
|
||||||
|
|
||||||
ColumnWithScrollBar(
|
LaunchedEffect(Unit) {
|
||||||
modifier = Modifier.fillMaxSize()
|
delay(300)
|
||||||
) {
|
focusRequester.requestFocus()
|
||||||
val displayName = rememberSaveable { mutableStateOf("") }
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
|
|
||||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false)
|
|
||||||
}
|
|
||||||
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
|
|
||||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
|
||||||
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
|
||||||
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
|
||||||
}
|
|
||||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
|
||||||
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
OnboardingActionButton(
|
|
||||||
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
|
||||||
labelId = MR.strings.create_profile_button,
|
|
||||||
onboarding = null,
|
|
||||||
enabled = canCreateProfile(displayName.value),
|
|
||||||
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
|
|
||||||
)
|
|
||||||
// Reserve space
|
|
||||||
TextButtonBelowOnboardingButton("", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
delay(300)
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -255,7 +233,6 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
|
|||||||
val modifier = Modifier
|
val modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 50.dp)
|
.heightIn(min = 50.dp)
|
||||||
.navigationBarsWithImePadding()
|
|
||||||
.onFocusChanged { focused = it.isFocused }
|
.onFocusChanged { focused = it.isFocused }
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -289,6 +266,7 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
|
|||||||
enabled = true,
|
enabled = true,
|
||||||
isError = false,
|
isError = false,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ fun IncomingCallAlertLayout(
|
|||||||
acceptCall: () -> Unit
|
acceptCall: () -> Unit
|
||||||
) {
|
) {
|
||||||
val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight
|
val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight
|
||||||
Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
|
Column(Modifier.fillMaxWidth().background(color).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
|
||||||
IncomingCallInfo(invitation, chatModel)
|
IncomingCallInfo(invitation, chatModel)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
|||||||
+1
-4
@@ -529,10 +529,7 @@ fun ChatInfoLayout(
|
|||||||
KeyChangeEffect(chat.id) {
|
KeyChangeEffect(chat.id) {
|
||||||
scope.launch { scrollState.scrollTo(0) }
|
scope.launch { scrollState.scrollTo(0) }
|
||||||
}
|
}
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
|
|||||||
+4
-4
@@ -276,7 +276,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryTab() {
|
fun HistoryTab() {
|
||||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
ColumnWithScrollBar {
|
||||||
Details()
|
Details()
|
||||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||||
val versions = ciInfo.itemVersions
|
val versions = ciInfo.itemVersions
|
||||||
@@ -300,7 +300,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun QuoteTab(qi: CIQuote) {
|
fun QuoteTab(qi: CIQuote) {
|
||||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
ColumnWithScrollBar {
|
||||||
Details()
|
Details()
|
||||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||||
@@ -313,7 +313,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ForwardedFromTab(forwardedFromItem: AChatItem) {
|
fun ForwardedFromTab(forwardedFromItem: AChatItem) {
|
||||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
ColumnWithScrollBar {
|
||||||
Details()
|
Details()
|
||||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||||
SectionView {
|
SectionView {
|
||||||
@@ -375,7 +375,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
|
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
|
||||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
ColumnWithScrollBar {
|
||||||
Details()
|
Details()
|
||||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||||
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
||||||
|
|||||||
+212
-216
@@ -12,10 +12,11 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.*
|
import androidx.compose.ui.*
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.*
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
@@ -108,6 +109,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
|
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) {
|
||||||
when (chatInfo) {
|
when (chatInfo) {
|
||||||
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
||||||
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
||||||
@@ -523,28 +525,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
|||||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||||
showSearch = showSearch
|
showSearch = showSearch
|
||||||
)
|
)
|
||||||
if (appPlatform.isAndroid) {
|
|
||||||
val backgroundColor = MaterialTheme.colors.background
|
|
||||||
val backgroundColorState = rememberUpdatedState(backgroundColor)
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
snapshotFlow { ModalManager.center.modalCount.value > 0 }
|
|
||||||
.collect { modalBackground ->
|
|
||||||
if (modalBackground) {
|
|
||||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
|
|
||||||
} else {
|
|
||||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ChatInfo.ContactConnection -> {
|
is ChatInfo.ContactConnection -> {
|
||||||
val close = { chatModel.chatId.value = null }
|
val close = { chatModel.chatId.value = null }
|
||||||
val handler = remember { AppBarHandler() }
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAppBarHandler provides handler
|
|
||||||
) {
|
|
||||||
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
||||||
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
|
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
|
||||||
})
|
})
|
||||||
@@ -553,14 +537,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
|||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
chatModel.chatItems.clear()
|
chatModel.chatItems.clear()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is ChatInfo.InvalidJSON -> {
|
is ChatInfo.InvalidJSON -> {
|
||||||
val close = { chatModel.chatId.value = null }
|
val close = { chatModel.chatId.value = null }
|
||||||
val handler = remember { AppBarHandler() }
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAppBarHandler provides handler
|
|
||||||
) {
|
|
||||||
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
|
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
|
||||||
InvalidJSONView(chatInfo.json)
|
InvalidJSONView(chatInfo.json)
|
||||||
})
|
})
|
||||||
@@ -569,10 +548,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
|||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
chatModel.chatItems.clear()
|
chatModel.chatItems.clear()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,81 +621,67 @@ fun ChatLayout(
|
|||||||
.desktopOnExternalDrag(
|
.desktopOnExternalDrag(
|
||||||
enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value,
|
enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value,
|
||||||
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
|
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
|
||||||
onImage = {
|
onImage = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } },
|
||||||
// TODO: file is not saved anywhere?!
|
|
||||||
val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
|
|
||||||
tmpFile.deleteOnExit()
|
|
||||||
chatModel.filesToDelete.add(tmpFile)
|
|
||||||
val uri = tmpFile.toURI()
|
|
||||||
CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) }
|
|
||||||
},
|
|
||||||
onText = {
|
onText = {
|
||||||
// Need to parse HTML in order to correctly display the content
|
// Need to parse HTML in order to correctly display the content
|
||||||
//composeState.value = composeState.value.copy(message = composeState.value.message + it)
|
//composeState.value = composeState.value.copy(message = composeState.value.message + it)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
ModalBottomSheetLayout(
|
||||||
ModalBottomSheetLayout(
|
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
sheetElevation = 0.dp,
|
||||||
modifier = Modifier.navigationBarsWithImePadding(),
|
sheetContent = {
|
||||||
sheetElevation = 0.dp,
|
ChooseAttachmentView(
|
||||||
sheetContent = {
|
attachmentOption,
|
||||||
ChooseAttachmentView(
|
hide = { scope.launch { attachmentBottomSheetState.hide() } }
|
||||||
attachmentOption,
|
)
|
||||||
hide = { scope.launch { attachmentBottomSheetState.hide() } }
|
},
|
||||||
)
|
sheetState = attachmentBottomSheetState,
|
||||||
},
|
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||||
sheetState = attachmentBottomSheetState,
|
) {
|
||||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
val composeViewHeight = remember { mutableStateOf(0.dp) }
|
||||||
) {
|
Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||||
val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) }
|
val remoteHostId = remember { remoteHostId }.value
|
||||||
val setFloatingButton = { button: @Composable () -> Unit ->
|
val chatInfo = remember { chatInfo }.value
|
||||||
floatingButton.value = button
|
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
|
||||||
}
|
if (chatInfo != null) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
if (selectedChatItems.value == null) {
|
|
||||||
val chatInfo = chatInfo.value
|
|
||||||
if (chatInfo != null) {
|
|
||||||
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SelectedItemsTopToolbar(selectedChatItems)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bottomBar = composeView,
|
|
||||||
modifier = Modifier.navigationBarsWithImePadding(),
|
|
||||||
floatingActionButton = { floatingButton.value() },
|
|
||||||
contentColor = LocalContentColor.current,
|
|
||||||
backgroundColor = Color.Unspecified
|
|
||||||
) { contentPadding ->
|
|
||||||
val wallpaperImage = MaterialTheme.wallpaper.type.image
|
|
||||||
val wallpaperType = MaterialTheme.wallpaper.type
|
|
||||||
val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background)
|
|
||||||
val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
|
||||||
BoxWithConstraints(Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colors.background)
|
|
||||||
.then(if (wallpaperImage != null)
|
|
||||||
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
|
|
||||||
else
|
|
||||||
Modifier)
|
|
||||||
.padding(contentPadding)
|
|
||||||
) {
|
|
||||||
val remoteHostId = remember { remoteHostId }.value
|
|
||||||
val chatInfo = remember { chatInfo }.value
|
|
||||||
if (chatInfo != null) {
|
|
||||||
ChatItemsList(
|
ChatItemsList(
|
||||||
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
|
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
|
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.imePadding()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.then(if (oneHandUI.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier)
|
||||||
|
) {
|
||||||
|
composeView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
if (oneHandUI.value) {
|
||||||
|
StatusBarBackground()
|
||||||
|
} else {
|
||||||
|
NavigationBarBackground(true, oneHandUI.value, noAlpha = true)
|
||||||
|
}
|
||||||
|
Box(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||||
|
if (selectedChatItems.value == null) {
|
||||||
|
if (chatInfo != null) {
|
||||||
|
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SelectedItemsTopToolbar(selectedChatItems)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -724,7 +689,7 @@ fun ChatLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInfoToolbar(
|
fun BoxScope.ChatInfoToolbar(
|
||||||
chatInfo: ChatInfo,
|
chatInfo: ChatInfo,
|
||||||
back: () -> Unit,
|
back: () -> Unit,
|
||||||
info: () -> Unit,
|
info: () -> Unit,
|
||||||
@@ -876,21 +841,33 @@ fun ChatInfoToolbar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
DefaultTopAppBar(
|
DefaultAppBar(
|
||||||
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
||||||
title = { ChatInfoToolbarTitle(chatInfo) },
|
title = { ChatInfoToolbarTitle(chatInfo) },
|
||||||
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
||||||
showSearch = showSearch.value,
|
showSearch = showSearch.value,
|
||||||
|
onTop = !oneHandUI.value,
|
||||||
onSearchValueChanged = onSearchValueChanged,
|
onSearchValueChanged = onSearchValueChanged,
|
||||||
buttons = barButtons
|
buttons = { barButtons.forEach { it() } }
|
||||||
)
|
)
|
||||||
|
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
||||||
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
val density = LocalDensity.current
|
||||||
|
val width = remember { mutableStateOf(250.dp) }
|
||||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) {
|
val height = remember { mutableStateOf(0.dp) }
|
||||||
DefaultDropdownMenu(showMenu) {
|
DefaultDropdownMenu(
|
||||||
menuItems.forEach { it() }
|
showMenu,
|
||||||
|
modifier = Modifier.onSizeChanged { with(density) {
|
||||||
|
width.value = it.width.toDp().coerceAtLeast(250.dp)
|
||||||
|
if (oneHandUI.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp()
|
||||||
|
} },
|
||||||
|
offset = DpOffset(-width.value, if (oneHandUI.value) -height.value else AppBarHeight)
|
||||||
|
) {
|
||||||
|
if (oneHandUI.value) {
|
||||||
|
menuItems.asReversed().forEach { it() }
|
||||||
|
} else {
|
||||||
|
menuItems.forEach { it() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -934,11 +911,12 @@ private fun ContactVerifiedShield() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxWithConstraintsScope.ChatItemsList(
|
fun BoxScope.ChatItemsList(
|
||||||
remoteHostId: Long?,
|
remoteHostId: Long?,
|
||||||
chatInfo: ChatInfo,
|
chatInfo: ChatInfo,
|
||||||
unreadCount: State<Int>,
|
unreadCount: State<Int>,
|
||||||
composeState: MutableState<ComposeState>,
|
composeState: MutableState<ComposeState>,
|
||||||
|
composeViewHeight: State<Dp>,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
useLinkPreviews: Boolean,
|
useLinkPreviews: Boolean,
|
||||||
linkMode: SimplexLinkMode,
|
linkMode: SimplexLinkMode,
|
||||||
@@ -963,7 +941,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
|
||||||
onComposed: suspend (chatId: String) -> Unit,
|
onComposed: suspend (chatId: String) -> Unit,
|
||||||
developerTools: Boolean,
|
developerTools: Boolean,
|
||||||
showViaProxy: Boolean
|
showViaProxy: Boolean
|
||||||
@@ -987,13 +964,18 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
||||||
|
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||||
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() })
|
||||||
val scrollToItem: (Long) -> Unit = { itemId: Long ->
|
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
|
||||||
val index = reversedChatItems.indexOfFirst { it.id == itemId }
|
val scrollToItem: State<(Long) -> Unit> = remember {
|
||||||
if (index != -1) {
|
mutableStateOf(
|
||||||
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) }
|
{ itemId: Long ->
|
||||||
}
|
val index = reversedChatItems.value.indexOfFirst { it.id == itemId }
|
||||||
|
if (index != -1) {
|
||||||
|
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
|
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
|
||||||
LaunchedEffect(chatInfo.id) {
|
LaunchedEffect(chatInfo.id) {
|
||||||
@@ -1011,8 +993,18 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
VideoPlayerHolder.releaseAll()
|
VideoPlayerHolder.releaseAll()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
LazyColumnWithScrollBar(
|
||||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
|
Modifier.align(Alignment.BottomCenter),
|
||||||
|
state = listState,
|
||||||
|
reverseLayout = true,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = topPaddingToContent(),
|
||||||
|
bottom = composeViewHeight.value
|
||||||
|
),
|
||||||
|
additionalBarOffset = composeViewHeight
|
||||||
|
) {
|
||||||
|
itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
|
||||||
|
val itemScope = rememberCoroutineScope()
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||||
@@ -1020,10 +1012,10 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
) {
|
) {
|
||||||
val provider = {
|
val provider = {
|
||||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||||
scope.launch {
|
itemScope.launch {
|
||||||
listState.scrollToItem(
|
listState.scrollToItem(
|
||||||
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
|
kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1),
|
||||||
-maxHeightRounded
|
-maxHeight.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1036,7 +1028,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||||
}) {
|
}) {
|
||||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1044,7 +1036,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
|
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
|
||||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||||
if (it == DismissValue.DismissedToStart) {
|
if (it == DismissValue.DismissedToStart) {
|
||||||
scope.launch {
|
itemScope.launch {
|
||||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
||||||
if (composeState.value.editing) {
|
if (composeState.value.editing) {
|
||||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||||
@@ -1241,16 +1233,17 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||||
|
val reversed = reversedChatItems.value
|
||||||
if (revealed.value && range != null) {
|
if (revealed.value && range != null) {
|
||||||
reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
|
reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
|
||||||
val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1]
|
val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1]
|
||||||
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
|
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
|
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == reversedChatItems.lastIndex) {
|
if (i == reversed.lastIndex) {
|
||||||
DateSeparator(cItem.meta.itemTs)
|
DateSeparator(cItem.meta.itemTs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1258,7 +1251,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
|
|
||||||
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
||||||
LaunchedEffect(cItem.id) {
|
LaunchedEffect(cItem.id) {
|
||||||
scope.launch {
|
itemScope.launch {
|
||||||
delay(600)
|
delay(600)
|
||||||
markRead(CC.ItemRange(cItem.id, cItem.id), null)
|
markRead(CC.ItemRange(cItem.id, cItem.id), null)
|
||||||
}
|
}
|
||||||
@@ -1267,10 +1260,10 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState)
|
FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState)
|
||||||
|
|
||||||
FloatingDate(
|
FloatingDate(
|
||||||
Modifier.padding(top = 10.dp).align(Alignment.TopCenter),
|
Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter),
|
||||||
listState,
|
listState,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1325,87 +1318,65 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxWithConstraintsScope.FloatingButtons(
|
fun BoxScope.FloatingButtons(
|
||||||
chatItems: State<List<ChatItem>>,
|
chatItems: State<List<ChatItem>>,
|
||||||
unreadCount: State<Int>,
|
unreadCount: State<Int>,
|
||||||
|
composeViewHeight: State<Dp>,
|
||||||
remoteHostId: Long?,
|
remoteHostId: Long?,
|
||||||
chatInfo: ChatInfo,
|
chatInfo: ChatInfo,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
|
||||||
listState: LazyListState
|
listState: LazyListState
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) }
|
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } }
|
||||||
var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) }
|
val bottomUnreadCount = remember {
|
||||||
var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) }
|
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
snapshotFlow { listState.firstVisibleItemIndex }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.collect {
|
|
||||||
firstVisibleIndex = it
|
|
||||||
firstItemIsVisible = firstVisibleIndex == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
// When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block
|
|
||||||
// so separate them into two LaunchedEffects
|
|
||||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.collect {
|
|
||||||
lastIndexOfVisibleItems = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val bottomUnreadCount by remember {
|
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (unreadCount.value == 0) return@derivedStateOf 0
|
if (unreadCount.value == 0) return@derivedStateOf 0
|
||||||
val items = chatItems.value
|
val items = chatItems.value
|
||||||
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
val from = items.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
|
||||||
if (items.size <= from || from < 0) return@derivedStateOf 0
|
if (items.size <= from || from < 0) return@derivedStateOf 0
|
||||||
|
|
||||||
items.subList(from, items.size).count { it.isRcvNew }
|
items.subList(from, items.size).count { it.isRcvNew }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
|
||||||
|
|
||||||
LaunchedEffect(bottomUnreadCount, firstItemIsVisible) {
|
val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } }
|
||||||
val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty()
|
val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } }
|
||||||
val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible
|
BottomEndFloatingButton(
|
||||||
setFloatingButton(
|
bottomUnreadCount,
|
||||||
bottomEndFloatingButton(
|
showBottomButtonWithCounter,
|
||||||
bottomUnreadCount,
|
showBottomButtonWithArrow,
|
||||||
showButtonWithCounter,
|
composeViewHeight,
|
||||||
showButtonWithArrow,
|
onClickArrowDown = {
|
||||||
onClickArrowDown = {
|
scope.launch { listState.animateScrollToItem(0) }
|
||||||
scope.launch { listState.animateScrollToItem(0) }
|
},
|
||||||
},
|
onClickCounter = {
|
||||||
onClickCounter = {
|
val firstVisibleOffset = (-maxHeight.value * 0.8).toInt()
|
||||||
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) }
|
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) }
|
||||||
}
|
}
|
||||||
))
|
)
|
||||||
}
|
|
||||||
// Don't show top FAB if is in search
|
// Don't show top FAB if is in search
|
||||||
if (searchValue.value.isNotEmpty()) return
|
if (searchValue.value.isNotEmpty()) return
|
||||||
val fabSize = 56.dp
|
val fabSize = 56.dp
|
||||||
val topUnreadCount by remember {
|
val topUnreadCount = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } }
|
||||||
derivedStateOf { unreadCount.value - bottomUnreadCount }
|
|
||||||
}
|
|
||||||
val showButtonWithCounter = topUnreadCount > 0
|
|
||||||
val height = with(LocalDensity.current) { maxHeight.toPx() }
|
|
||||||
val showDropDown = remember { mutableStateOf(false) }
|
val showDropDown = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
TopEndFloatingButton(
|
TopEndFloatingButton(
|
||||||
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd),
|
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd),
|
||||||
topUnreadCount,
|
topUnreadCount,
|
||||||
showButtonWithCounter,
|
onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } },
|
||||||
onClick = { scope.launch { listState.animateScrollBy(height) } },
|
|
||||||
onLongClick = { showDropDown.value = true }
|
onLongClick = { showDropDown.value = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
Box {
|
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
||||||
DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
|
val density = LocalDensity.current
|
||||||
|
val width = remember { mutableStateOf(250.dp) }
|
||||||
|
DefaultDropdownMenu(
|
||||||
|
showDropDown,
|
||||||
|
modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } },
|
||||||
|
offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent())
|
||||||
|
) {
|
||||||
ItemAction(
|
ItemAction(
|
||||||
generalGetString(MR.strings.mark_read),
|
generalGetString(MR.strings.mark_read),
|
||||||
painterResource(MR.images.ic_check),
|
painterResource(MR.images.ic_check),
|
||||||
@@ -1413,7 +1384,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
|||||||
val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction
|
val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction
|
||||||
markRead(
|
markRead(
|
||||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||||
bottomUnreadCount
|
bottomUnreadCount.value
|
||||||
)
|
)
|
||||||
showDropDown.value = false
|
showDropDown.value = false
|
||||||
})
|
})
|
||||||
@@ -1475,12 +1446,11 @@ fun MemberImage(member: GroupMember) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TopEndFloatingButton(
|
private fun TopEndFloatingButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
unreadCount: Int,
|
unreadCount: State<Int>,
|
||||||
showButtonWithCounter: Boolean,
|
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit
|
onLongClick: () -> Unit
|
||||||
) = when {
|
) = when {
|
||||||
showButtonWithCounter -> {
|
unreadCount.value > 0 -> {
|
||||||
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
|
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
{}, // no action here
|
{}, // no action here
|
||||||
@@ -1490,7 +1460,7 @@ private fun TopEndFloatingButton(
|
|||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
unreadCountStr(unreadCount),
|
unreadCountStr(unreadCount.value),
|
||||||
color = MaterialTheme.colors.primary,
|
color = MaterialTheme.colors.primary,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
@@ -1500,6 +1470,16 @@ private fun TopEndFloatingButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun topPaddingToContent(): Dp {
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
return if (oneHandUI.value) {
|
||||||
|
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
|
} else {
|
||||||
|
AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FloatingDate(
|
private fun FloatingDate(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
@@ -1509,8 +1489,9 @@ private fun FloatingDate(
|
|||||||
var isNearBottom by remember { mutableStateOf(true) }
|
var isNearBottom by remember { mutableStateOf(true) }
|
||||||
val lastVisibleItemDate = remember {
|
val lastVisibleItemDate = remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) {
|
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0) {
|
||||||
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
|
val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset
|
||||||
|
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - (listState.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset + item.size <= lastFullyVisibleOffset && item.size > 0 }?.index ?: 0)
|
||||||
val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex)
|
val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex)
|
||||||
val timeZone = TimeZone.currentSystemDefault()
|
val timeZone = TimeZone.currentSystemDefault()
|
||||||
item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
|
item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
|
||||||
@@ -1697,48 +1678,44 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bottomEndFloatingButton(
|
@Composable
|
||||||
unreadCount: Int,
|
private fun BoxScope.BottomEndFloatingButton(
|
||||||
showButtonWithCounter: Boolean,
|
unreadCount: State<Int>,
|
||||||
showButtonWithArrow: Boolean,
|
showButtonWithCounter: State<Boolean>,
|
||||||
|
showButtonWithArrow: State<Boolean>,
|
||||||
|
composeViewHeight: State<Dp>,
|
||||||
onClickArrowDown: () -> Unit,
|
onClickArrowDown: () -> Unit,
|
||||||
onClickCounter: () -> Unit
|
onClickCounter: () -> Unit
|
||||||
): @Composable () -> Unit = when {
|
) = when {
|
||||||
showButtonWithCounter -> {
|
showButtonWithCounter.value -> {
|
||||||
{
|
FloatingActionButton(
|
||||||
FloatingActionButton(
|
onClick = onClickCounter,
|
||||||
onClick = onClickCounter,
|
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||||
modifier = Modifier.size(48.dp),
|
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
) {
|
||||||
) {
|
Text(
|
||||||
Text(
|
unreadCountStr(unreadCount.value),
|
||||||
unreadCountStr(unreadCount),
|
color = MaterialTheme.colors.primary,
|
||||||
color = MaterialTheme.colors.primary,
|
fontSize = 14.sp,
|
||||||
fontSize = 14.sp,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showButtonWithArrow -> {
|
showButtonWithArrow.value -> {
|
||||||
{
|
FloatingActionButton(
|
||||||
FloatingActionButton(
|
onClick = onClickArrowDown,
|
||||||
onClick = onClickArrowDown,
|
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||||
modifier = Modifier.size(48.dp),
|
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
) {
|
||||||
) {
|
Icon(
|
||||||
Icon(
|
painter = painterResource(MR.images.ic_keyboard_arrow_down),
|
||||||
painter = painterResource(MR.images.ic_keyboard_arrow_down),
|
contentDescription = null,
|
||||||
contentDescription = null,
|
tint = MaterialTheme.colors.primary
|
||||||
tint = MaterialTheme.colors.primary
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {}
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1865,6 +1842,25 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Modifier.chatViewBackgroundModifier(
|
||||||
|
colors: Colors,
|
||||||
|
wallpaper: AppWallpaper,
|
||||||
|
backgroundGraphicsLayerSize: MutableState<IntSize>?,
|
||||||
|
backgroundGraphicsLayer: GraphicsLayer?
|
||||||
|
): Modifier {
|
||||||
|
val wallpaperImage = wallpaper.type.image
|
||||||
|
val wallpaperType = wallpaper.type
|
||||||
|
val backgroundColor = wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background)
|
||||||
|
val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
||||||
|
|
||||||
|
return this
|
||||||
|
.then(if (wallpaperImage != null)
|
||||||
|
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) }
|
||||||
|
else
|
||||||
|
Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? =
|
fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? =
|
||||||
if (currIndex != null && prevHidden != null && prevHidden > currIndex) {
|
if (currIndex != null && prevHidden != null && prevHidden > currIndex) {
|
||||||
currIndex..prevHidden
|
currIndex..prevHidden
|
||||||
|
|||||||
+4
-2
@@ -13,12 +13,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.model.ChatModel.controller
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
import chat.simplex.common.model.ChatModel.filesToDelete
|
import chat.simplex.common.model.ChatModel.filesToDelete
|
||||||
import chat.simplex.common.model.ChatModel.withChats
|
import chat.simplex.common.model.ChatModel.withChats
|
||||||
@@ -896,7 +898,7 @@ fun ComposeView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
Box(Modifier.background(MaterialTheme.colors.background)) {
|
||||||
Divider()
|
Divider()
|
||||||
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
|
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
|
||||||
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
|
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
|
||||||
@@ -918,7 +920,7 @@ fun ComposeView(
|
|||||||
&& !nextSendGrpInv.value
|
&& !nextSendGrpInv.value
|
||||||
IconButton(
|
IconButton(
|
||||||
attachmentClicked,
|
attachmentClicked,
|
||||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
|
Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
|
||||||
enabled = attachmentEnabled
|
enabled = attachmentEnabled
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
+1
-4
@@ -81,10 +81,7 @@ private fun ContactPreferencesLayout(
|
|||||||
reset: () -> Unit,
|
reset: () -> Unit,
|
||||||
savePrefs: () -> Unit,
|
savePrefs: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.contact_preferences))
|
AppBarTitle(stringResource(MR.strings.contact_preferences))
|
||||||
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
|
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
|
||||||
val onTTLUpdated = { ttl: Int? ->
|
val onTTLUpdated = { ttl: Int? ->
|
||||||
|
|||||||
+4
-3
@@ -1,9 +1,11 @@
|
|||||||
package chat.simplex.common.views.chat
|
package chat.simplex.common.views.chat
|
||||||
|
|
||||||
|
import SectionBottomSpacer
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||||
@@ -12,9 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||||
Column(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.scan_code))
|
AppBarTitle(stringResource(MR.strings.scan_code))
|
||||||
QRCodeScanner { text ->
|
QRCodeScanner { text ->
|
||||||
verifyCode(text) {
|
verifyCode(text) {
|
||||||
@@ -28,5 +28,6 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: ()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING))
|
Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING))
|
||||||
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.platform.BackHandler
|
import chat.simplex.common.platform.BackHandler
|
||||||
import chat.simplex.common.platform.chatModel
|
import chat.simplex.common.platform.chatModel
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -20,11 +21,12 @@ import chat.simplex.res.MR
|
|||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
||||||
val onBackClicked = { selectedChatItems.value = null }
|
val onBackClicked = { selectedChatItems.value = null }
|
||||||
BackHandler(onBack = onBackClicked)
|
BackHandler(onBack = onBackClicked)
|
||||||
val count = selectedChatItems.value?.size ?: 0
|
val count = selectedChatItems.value?.size ?: 0
|
||||||
DefaultTopAppBar(
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
DefaultAppBar(
|
||||||
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
@@ -39,10 +41,9 @@ fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onTitleClick = null,
|
onTitleClick = null,
|
||||||
showSearch = false,
|
onTop = !oneHandUI.value,
|
||||||
onSearchValueChanged = {},
|
onSearchValueChanged = {},
|
||||||
)
|
)
|
||||||
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -68,6 +69,8 @@ fun SelectedItemsBottomToolbar(
|
|||||||
Modifier
|
Modifier
|
||||||
.matchParentSize()
|
.matchParentSize()
|
||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectGesture {
|
detectGesture {
|
||||||
true
|
true
|
||||||
@@ -103,6 +106,7 @@ fun SelectedItemsBottomToolbar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Divider(Modifier.align(Alignment.TopStart))
|
||||||
}
|
}
|
||||||
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
||||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||||
|
|||||||
+8
-5
@@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.*
|
import androidx.compose.foundation.shape.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.*
|
import androidx.compose.ui.*
|
||||||
@@ -15,6 +14,7 @@ import androidx.compose.ui.draw.alpha
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
@@ -61,7 +61,8 @@ fun SendMsgView(
|
|||||||
) {
|
) {
|
||||||
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) {
|
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
|
val cs = composeState.value
|
||||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||||
LaunchedEffect(composeState.value.inProgress) {
|
LaunchedEffect(composeState.value.inProgress) {
|
||||||
@@ -147,7 +148,7 @@ fun SendMsgView(
|
|||||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||||
&& cs.contextItem is ComposeContextItem.NoContextItem
|
&& cs.contextItem is ComposeContextItem.NoContextItem
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.width(10.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
StartLiveMessageButton(userCanSend) {
|
StartLiveMessageButton(userCanSend) {
|
||||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||||
@@ -423,6 +424,7 @@ private fun SendMsgButton(
|
|||||||
onLongClick: (() -> Unit)? = null
|
onLongClick: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.requiredSize(36.dp)
|
modifier = Modifier.requiredSize(36.dp)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
@@ -431,7 +433,7 @@ private fun SendMsgButton(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
indication = ripple
|
||||||
)
|
)
|
||||||
.onRightClick { onLongClick?.invoke() },
|
.onRightClick { onLongClick?.invoke() },
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -454,6 +456,7 @@ private fun SendMsgButton(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.requiredSize(36.dp)
|
modifier = Modifier.requiredSize(36.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
@@ -461,7 +464,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
indication = ripple
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
|||||||
+1
-5
@@ -56,11 +56,7 @@ private fun VerifyCodeLayout(
|
|||||||
connectionVerified: Boolean,
|
connectionVerified: Boolean,
|
||||||
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
|
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = DEFAULT_PADDING)
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
|
AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
|
||||||
val splitCode = splitToParts(connectionCode, 24)
|
val splitCode = splitToParts(connectionCode, 24)
|
||||||
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
|
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
|
||||||
|
|||||||
+1
-4
@@ -130,10 +130,7 @@ fun AddGroupMembersLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.button_add_members))
|
AppBarTitle(stringResource(MR.strings.button_add_members))
|
||||||
profileText()
|
profileText()
|
||||||
Spacer(Modifier.size(DEFAULT_PADDING))
|
Spacer(Modifier.size(DEFAULT_PADDING))
|
||||||
|
|||||||
+13
-2
@@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.model.ChatModel.withChats
|
import chat.simplex.common.model.ChatModel.withChats
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -283,9 +284,14 @@ fun ModalData.GroupChatInfoLayout(
|
|||||||
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
|
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Box {
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
LazyColumnWithScrollBar(
|
LazyColumnWithScrollBar(
|
||||||
Modifier
|
contentPadding = if (oneHandUI.value) {
|
||||||
.fillMaxWidth(),
|
PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
|
||||||
|
} else {
|
||||||
|
PaddingValues(top = topPaddingToContent())
|
||||||
|
},
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
@@ -397,6 +403,11 @@ fun ModalData.GroupChatInfoLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!oneHandUI.value) {
|
||||||
|
NavigationBarBackground(oneHandUI.value, oneHandUI.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -119,9 +119,7 @@ fun GroupLinkLayout(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier,
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.group_link))
|
AppBarTitle(stringResource(MR.strings.group_link))
|
||||||
Text(
|
Text(
|
||||||
stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
|
stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
|
||||||
|
|||||||
+1
-4
@@ -313,10 +313,7 @@ fun GroupMemberInfoLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
|
|||||||
+1
-3
@@ -82,9 +82,7 @@ private fun GroupPreferencesLayout(
|
|||||||
reset: () -> Unit,
|
reset: () -> Unit,
|
||||||
savePrefs: () -> Unit,
|
savePrefs: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.group_preferences))
|
AppBarTitle(stringResource(MR.strings.group_preferences))
|
||||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
|
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
|
||||||
val onTTLUpdated = { ttl: Int? ->
|
val onTTLUpdated = { ttl: Int? ->
|
||||||
|
|||||||
+2
-6
@@ -82,10 +82,9 @@ fun GroupProfileLayout(
|
|||||||
}, close)
|
}, close)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
|
||||||
ModalBottomSheetLayout(
|
ModalBottomSheetLayout(
|
||||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||||
modifier = Modifier.navigationBarsWithImePadding(),
|
modifier = Modifier.imePadding(),
|
||||||
sheetContent = {
|
sheetContent = {
|
||||||
GetImageBottomSheet(
|
GetImageBottomSheet(
|
||||||
chosenImage,
|
chosenImage,
|
||||||
@@ -98,9 +97,7 @@ fun GroupProfileLayout(
|
|||||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||||
) {
|
) {
|
||||||
ModalView(close = closeWithAlert) {
|
ModalView(close = closeWithAlert) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.padding(horizontal = DEFAULT_PADDING)
|
.padding(horizontal = DEFAULT_PADDING)
|
||||||
@@ -177,7 +174,6 @@ fun GroupProfileLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean =
|
private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean =
|
||||||
|
|||||||
+1
-3
@@ -95,9 +95,7 @@ private fun GroupWelcomeLayout(
|
|||||||
linkMode: SimplexLinkMode,
|
linkMode: SimplexLinkMode,
|
||||||
save: () -> Unit,
|
save: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
val editMode = remember { mutableStateOf(true) }
|
val editMode = remember { mutableStateOf(true) }
|
||||||
AppBarTitle(stringResource(MR.strings.group_welcome_title))
|
AppBarTitle(stringResource(MR.strings.group_welcome_title))
|
||||||
val wt = rememberSaveable { welcomeText }
|
val wt = rememberSaveable { welcomeText }
|
||||||
|
|||||||
+67
@@ -10,6 +10,7 @@ import androidx.compose.ui.draw.clipToBounds
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.layout.*
|
import androidx.compose.ui.layout.*
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.UriHandler
|
import androidx.compose.ui.platform.UriHandler
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
@@ -320,6 +321,8 @@ fun CIMarkdownText(
|
|||||||
|
|
||||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||||
const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble"
|
const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble"
|
||||||
|
const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose"
|
||||||
|
const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose"
|
||||||
/**
|
/**
|
||||||
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
|
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
|
||||||
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
|
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
|
||||||
@@ -398,6 +401,70 @@ fun DependentLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect).
|
||||||
|
// So, steps are:
|
||||||
|
// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0)
|
||||||
|
// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height
|
||||||
|
// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size
|
||||||
|
// - every added/removed line in compose view goes through the same process.
|
||||||
|
@Composable
|
||||||
|
fun AdaptingBottomPaddingLayout(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
mainLayoutId: String,
|
||||||
|
expectedHeight: MutableState<Dp>,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() }
|
||||||
|
Layout(
|
||||||
|
content = content,
|
||||||
|
modifier = modifier
|
||||||
|
) { measureable, constraints ->
|
||||||
|
require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" }
|
||||||
|
val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints)
|
||||||
|
val placeables: List<Placeable> = measureable.map {
|
||||||
|
if (it.layoutId == mainLayoutId)
|
||||||
|
mainPlaceable
|
||||||
|
else
|
||||||
|
it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) }
|
||||||
|
expectedHeight.value = mainPlaceable.measuredHeight.toDp()
|
||||||
|
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||||
|
var y = 0
|
||||||
|
placeables.forEach {
|
||||||
|
if (it !== mainPlaceable) {
|
||||||
|
it.place(0, y)
|
||||||
|
y += it.measuredHeight
|
||||||
|
} else {
|
||||||
|
it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight)
|
||||||
|
y += it.measuredHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CenteredRowLayout(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Layout(
|
||||||
|
content = content,
|
||||||
|
modifier = modifier
|
||||||
|
) { measureable, constraints ->
|
||||||
|
require(measureable.size == 3) { "Should be exactly three elements in this layout, you have ${measureable.size}" }
|
||||||
|
val first = measureable[0].measure(constraints.copy(minWidth = 0, minHeight = 0))
|
||||||
|
val third = measureable[2].measure(constraints.copy(minWidth = first.measuredWidth, minHeight = 0))
|
||||||
|
val second = measureable[1].measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = (constraints.maxWidth - first.measuredWidth - third.measuredWidth).coerceAtLeast(0)))
|
||||||
|
// Limit width for every other element to width of important element and height for a sum of all elements.
|
||||||
|
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||||
|
first.place(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0))
|
||||||
|
second.place((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0))
|
||||||
|
third.place(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
class EditedProvider: PreviewParameterProvider<Boolean> {
|
class EditedProvider: PreviewParameterProvider<Boolean> {
|
||||||
|
|||||||
+10
-3
@@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.input.pointer.*
|
import androidx.compose.ui.input.pointer.*
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
|
||||||
import chat.simplex.common.model.CryptoFile
|
import chat.simplex.common.model.CryptoFile
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.CurrentColors
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
@@ -58,9 +57,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
|||||||
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
|
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
|
||||||
DisposableEffectOnGone(
|
DisposableEffectOnGone(
|
||||||
always = {
|
always = {
|
||||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false)
|
platform.androidSetStatusAndNavigationBarAppearance(false, false, blackNavBar = true)
|
||||||
|
chatModel.fullscreenGalleryVisible.value = true
|
||||||
},
|
},
|
||||||
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
|
whenDispose = {
|
||||||
|
val c = CurrentColors.value.colors
|
||||||
|
platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
|
||||||
|
chatModel.fullscreenGalleryVisible.value = false
|
||||||
|
},
|
||||||
|
whenGone = {
|
||||||
|
playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+229
-164
@@ -10,8 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.*
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.*
|
import androidx.compose.ui.focus.*
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
@@ -34,6 +33,7 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.call.Call
|
import chat.simplex.common.views.call.Call
|
||||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||||
|
import chat.simplex.common.views.chat.topPaddingToContent
|
||||||
import chat.simplex.common.views.newchat.*
|
import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.common.views.usersettings.*
|
import chat.simplex.common.views.usersettings.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -41,7 +41,6 @@ import kotlinx.coroutines.*
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.net.URI
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
||||||
@@ -55,7 +54,7 @@ private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
|||||||
chatModel.newChatSheetVisible.value = false
|
chatModel.newChatSheetVisible.value = false
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
ModalView(close, closeOnTop = !oneHandUI.value) {
|
ModalView(close, showAppBar = !oneHandUI.value) {
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
BackHandler {
|
BackHandler {
|
||||||
close()
|
close()
|
||||||
@@ -122,11 +121,7 @@ fun ToggleChatListCard() {
|
|||||||
|
|
||||||
SharedPreferenceToggle(
|
SharedPreferenceToggle(
|
||||||
appPrefs.oneHandUI,
|
appPrefs.oneHandUI,
|
||||||
enabled = true,
|
enabled = true
|
||||||
onChange = {
|
|
||||||
val c = CurrentColors.value.colors
|
|
||||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,74 +149,36 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||||
Scaffold(
|
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||||
topBar = {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (!oneHandUI.value) {
|
if (oneHandUI.value) {
|
||||||
Column {
|
ChatListWithLoadingScreen(searchText, listState)
|
||||||
ChatListToolbar(
|
Column(Modifier.align(Alignment.BottomCenter)) {
|
||||||
userPickerState,
|
ChatListToolbar(
|
||||||
stopped,
|
userPickerState,
|
||||||
setPerformLA,
|
listState,
|
||||||
)
|
stopped,
|
||||||
Divider()
|
setPerformLA,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
bottomBar = {
|
ChatListWithLoadingScreen(searchText, listState)
|
||||||
if (oneHandUI.value) {
|
Column {
|
||||||
Column {
|
ChatListToolbar(
|
||||||
Divider()
|
userPickerState,
|
||||||
ChatListToolbar(
|
listState,
|
||||||
userPickerState,
|
stopped,
|
||||||
stopped,
|
setPerformLA,
|
||||||
setPerformLA,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||||
contentColor = LocalContentColor.current,
|
NewChatSheetFloatingButton(oneHandUI, stopped)
|
||||||
floatingActionButton = {
|
|
||||||
if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
if (!stopped) {
|
|
||||||
showNewChatSheet(oneHandUI)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Modifier
|
|
||||||
.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp)
|
|
||||||
.size(AppBarHeight * fontSizeSqrtMultiplier),
|
|
||||||
elevation = FloatingActionButtonDefaults.elevation(
|
|
||||||
defaultElevation = 0.dp,
|
|
||||||
pressedElevation = 0.dp,
|
|
||||||
hoveredElevation = 0.dp,
|
|
||||||
focusedElevation = 0.dp,
|
|
||||||
),
|
|
||||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
|
||||||
contentColor = Color.White
|
|
||||||
) {
|
|
||||||
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box(Modifier.padding(it)) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
if (!chatModel.desktopNoUserNoRemote) {
|
|
||||||
ChatList(chatModel, searchText = searchText)
|
|
||||||
}
|
|
||||||
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
|
||||||
Text(stringResource(
|
|
||||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchText.value.text.isEmpty()) {
|
if (searchText.value.text.isEmpty()) {
|
||||||
if (appPlatform.isDesktop) {
|
if (appPlatform.isDesktop && !oneHandUI.value) {
|
||||||
val call = remember { chatModel.activeCall }.value
|
val call = remember { chatModel.activeCall }.value
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
ActiveCallInteractiveArea(call)
|
ActiveCallInteractiveArea(call)
|
||||||
@@ -239,6 +196,46 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState<TextFieldValue>, listState: LazyListState) {
|
||||||
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
|
ChatList(searchText = searchText, listState)
|
||||||
|
}
|
||||||
|
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats
|
||||||
|
), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State<Boolean>, stopped: Boolean) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
if (!stopped) {
|
||||||
|
showNewChatSheet(oneHandUI)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Modifier
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(
|
||||||
|
defaultElevation = 0.dp,
|
||||||
|
pressedElevation = 0.dp,
|
||||||
|
hoveredElevation = 0.dp,
|
||||||
|
focusedElevation = 0.dp,
|
||||||
|
),
|
||||||
|
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||||
|
contentColor = Color.White
|
||||||
|
) {
|
||||||
|
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConnectButton(text: String, onClick: () -> Unit) {
|
private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||||
Button(
|
Button(
|
||||||
@@ -256,7 +253,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, setPerformLA: (Boolean) -> Unit) {
|
private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) {
|
||||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||||
val updatingProgress = remember { chatModel.updatingProgress }.value
|
val updatingProgress = remember { chatModel.updatingProgress }.value
|
||||||
@@ -265,6 +262,18 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
|||||||
if (oneHandUI.value) {
|
if (oneHandUI.value) {
|
||||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
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 (!stopped) {
|
if (!stopped) {
|
||||||
barButtons.add {
|
barButtons.add {
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -323,7 +332,9 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
DefaultTopAppBar(
|
val scope = rememberCoroutineScope()
|
||||||
|
val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } }
|
||||||
|
DefaultAppBar(
|
||||||
navigationButton = {
|
navigationButton = {
|
||||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||||
NavigationButtonMenu {
|
NavigationButtonMenu {
|
||||||
@@ -351,15 +362,14 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
|||||||
SubscriptionStatusIndicator(
|
SubscriptionStatusIndicator(
|
||||||
click = {
|
click = {
|
||||||
ModalManager.start.closeModals()
|
ModalManager.start.closeModals()
|
||||||
|
val summary = serversSummary.value
|
||||||
ModalManager.start.showModalCloseable(
|
ModalManager.start.showModalCloseable(
|
||||||
endButtons = {
|
endButtons = {
|
||||||
val summary = serversSummary.value
|
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
ShareButton {
|
ShareButton {
|
||||||
val json = Json {
|
val json = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
|
val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
|
||||||
clipboard.shareText(text)
|
clipboard.shareText(text)
|
||||||
}
|
}
|
||||||
@@ -370,10 +380,10 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTitleClick = null,
|
onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null,
|
||||||
showSearch = false,
|
onTop = !oneHandUI.value,
|
||||||
onSearchValueChanged = {},
|
onSearchValueChanged = {},
|
||||||
buttons = barButtons
|
buttons = { barButtons.forEach { it() } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,74 +501,78 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Box {
|
||||||
val focusRequester = remember { FocusRequester() }
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
var focused by remember { mutableStateOf(false) }
|
val focusRequester = remember { FocusRequester() }
|
||||||
Icon(
|
var focused by remember { mutableStateOf(false) }
|
||||||
painterResource(MR.images.ic_search),
|
Icon(
|
||||||
contentDescription = null,
|
painterResource(MR.images.ic_search),
|
||||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier),
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colors.secondary
|
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier),
|
||||||
)
|
tint = MaterialTheme.colors.secondary
|
||||||
SearchTextField(
|
)
|
||||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
SearchTextField(
|
||||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||||
alwaysVisible = true,
|
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||||
searchText = searchText,
|
alwaysVisible = true,
|
||||||
enabled = !remember { searchShowingSimplexLink }.value,
|
searchText = searchText,
|
||||||
trailingContent = null,
|
enabled = !remember { searchShowingSimplexLink }.value,
|
||||||
) {
|
trailingContent = null,
|
||||||
searchText.value = searchText.value.copy(it)
|
) {
|
||||||
}
|
searchText.value = searchText.value.copy(it)
|
||||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
|
||||||
if (hasText.value) {
|
|
||||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
|
||||||
BackHandler(onBack = hideSearchOnBack)
|
|
||||||
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
|
||||||
hideSearchOnBack()
|
|
||||||
}
|
}
|
||||||
} else {
|
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
if (hasText.value) {
|
||||||
if (chatModel.chats.value.isNotEmpty()) {
|
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||||
ToggleFilterEnabledButton()
|
BackHandler(onBack = hideSearchOnBack)
|
||||||
}
|
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
||||||
Spacer(Modifier.width(padding))
|
hideSearchOnBack()
|
||||||
}
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val keyboardState = getKeyboardState()
|
|
||||||
LaunchedEffect(keyboardState.value) {
|
|
||||||
if (keyboardState.value == KeyboardState.Closed && focused) {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val view = LocalMultiplatformView()
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
snapshotFlow { searchText.value.text }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.collect {
|
|
||||||
val link = strHasSingleSimplexLink(it.trim())
|
|
||||||
if (link != null) {
|
|
||||||
// if SimpleX link is pasted, show connection dialogue
|
|
||||||
hideKeyboard(view)
|
|
||||||
if (link.format is Format.SimplexLink) {
|
|
||||||
val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts)
|
|
||||||
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
|
|
||||||
}
|
|
||||||
searchShowingSimplexLink.value = true
|
|
||||||
searchChatFilteredBySimplexLink.value = null
|
|
||||||
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
|
|
||||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
|
||||||
if (it.isNotEmpty()) {
|
|
||||||
// if some other text is pasted, enter search mode
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
|
||||||
listState.scrollToItem(0)
|
|
||||||
}
|
|
||||||
searchShowingSimplexLink.value = false
|
|
||||||
searchChatFilteredBySimplexLink.value = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||||
|
if (chatModel.chats.value.isNotEmpty()) {
|
||||||
|
ToggleFilterEnabledButton()
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(padding))
|
||||||
|
}
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardState = getKeyboardState()
|
||||||
|
LaunchedEffect(keyboardState.value) {
|
||||||
|
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val view = LocalMultiplatformView()
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { searchText.value.text }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect {
|
||||||
|
val link = strHasSingleSimplexLink(it.trim())
|
||||||
|
if (link != null) {
|
||||||
|
// if SimpleX link is pasted, show connection dialogue
|
||||||
|
hideKeyboard(view)
|
||||||
|
if (link.format is Format.SimplexLink) {
|
||||||
|
val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts)
|
||||||
|
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||||
|
}
|
||||||
|
searchShowingSimplexLink.value = true
|
||||||
|
searchChatFilteredBySimplexLink.value = null
|
||||||
|
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
|
||||||
|
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
// if some other text is pasted, enter search mode
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
searchShowingSimplexLink.value = false
|
||||||
|
searchChatFilteredBySimplexLink.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,8 +604,37 @@ enum class ScrollDirection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
fun BoxScope.StatusBarBackground() {
|
||||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
if (appPlatform.isAndroid) {
|
||||||
|
val finalColor = MaterialTheme.colors.background.copy(0.88f)
|
||||||
|
Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) {
|
||||||
|
if (appPlatform.isAndroid) {
|
||||||
|
val barPadding = WindowInsets.navigationBars.asPaddingValues()
|
||||||
|
val paddingBottom = barPadding.calculateBottomPadding()
|
||||||
|
val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background
|
||||||
|
val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f)
|
||||||
|
Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) {
|
||||||
|
val keyboardState = getKeyboardState()
|
||||||
|
if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) {
|
||||||
|
val barPadding = WindowInsets.navigationBars.asPaddingValues()
|
||||||
|
val paddingBottom = barPadding.calculateBottomPadding()
|
||||||
|
val finalColor = color.copy(0.6f)
|
||||||
|
Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listState: LazyListState) {
|
||||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||||
var previousIndex by remember { mutableStateOf(0) }
|
var previousIndex by remember { mutableStateOf(0) }
|
||||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||||
@@ -628,40 +671,45 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
|||||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
|
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
|
||||||
|
val topPaddingToContent = topPaddingToContent()
|
||||||
|
val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
|
||||||
LazyColumnWithScrollBar(
|
LazyColumnWithScrollBar(
|
||||||
Modifier.fillMaxSize(),
|
if (!oneHandUI.value) Modifier.imePadding() else Modifier,
|
||||||
listState,
|
listState,
|
||||||
reverseLayout = oneHandUI.value
|
reverseLayout = oneHandUI.value
|
||||||
) {
|
) {
|
||||||
|
item { Spacer(Modifier.height(blankSpaceSize)) }
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
.zIndex(1f)
|
||||||
.offset {
|
.offset {
|
||||||
val y = if (searchText.value.text.isEmpty()) {
|
val offsetMultiplier = if (oneHandUI.value) 1 else -1
|
||||||
val offsetMultiplier = if (oneHandUI.value) 1 else -1
|
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) {
|
||||||
if (
|
if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset
|
||||||
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
|
else -offsetMultiplier * blankSpaceSize.roundToPx()
|
||||||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
|
|
||||||
) {
|
|
||||||
0
|
|
||||||
} else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000
|
|
||||||
} else {
|
} else {
|
||||||
0
|
when (listState.firstVisibleItemIndex) {
|
||||||
|
0 -> 0
|
||||||
|
1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset
|
||||||
|
else -> offsetMultiplier * 1000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
IntOffset(0, y)
|
IntOffset(0, y)
|
||||||
}
|
}
|
||||||
.background(MaterialTheme.colors.background),
|
.background(MaterialTheme.colors.background)
|
||||||
) {
|
) {
|
||||||
if (oneHandUI.value) {
|
if (oneHandUI.value) {
|
||||||
Divider()
|
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
|
||||||
}
|
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||||
if (!oneHandUI.value) {
|
}
|
||||||
Divider()
|
} else {
|
||||||
|
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() > 1) {
|
if (!oneHandUICardShown.value && chats.size > 1) {
|
||||||
item {
|
item {
|
||||||
ToggleChatListCard()
|
ToggleChatListCard()
|
||||||
}
|
}
|
||||||
@@ -672,17 +720,30 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
|||||||
} }
|
} }
|
||||||
ChatListNavLinkView(chat, nextChatSelected)
|
ChatListNavLinkView(chat, nextChatSelected)
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() <= 1) {
|
if (!oneHandUICardShown.value && chats.size <= 1) {
|
||||||
item {
|
item {
|
||||||
ToggleChatListCard()
|
ToggleChatListCard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (appPlatform.isAndroid) {
|
||||||
|
item { Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
|
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) {
|
||||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oneHandUI.value) {
|
||||||
|
StatusBarBackground()
|
||||||
|
} else {
|
||||||
|
NavigationBarBackground(oneHandUI.value, true)
|
||||||
|
}
|
||||||
|
if (!oneHandUICardShown.value) {
|
||||||
|
LaunchedEffect(chats.size) {
|
||||||
|
if (chats.size >= 3) appPrefs.oneHandUICardShown.set(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun filteredChats(
|
fun filteredChats(
|
||||||
@@ -727,3 +788,7 @@ private fun filtered(chat: Chat): Boolean =
|
|||||||
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
||||||
chat.chatStats.unreadChat ||
|
chat.chatStats.unreadChat ||
|
||||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||||
|
|
||||||
|
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {
|
||||||
|
scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } }
|
||||||
|
}
|
||||||
|
|||||||
+5
-15
@@ -620,9 +620,7 @@ fun ModalData.SMPServerSummaryView(
|
|||||||
ModalView(
|
ModalView(
|
||||||
close = close
|
close = close
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
val bottomPadding = DEFAULT_PADDING
|
val bottomPadding = DEFAULT_PADDING
|
||||||
AppBarTitle(
|
AppBarTitle(
|
||||||
stringResource(MR.strings.smp_server),
|
stringResource(MR.strings.smp_server),
|
||||||
@@ -645,9 +643,7 @@ fun ModalData.DetailedXFTPStatsView(
|
|||||||
ModalView(
|
ModalView(
|
||||||
close = close
|
close = close
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
val bottomPadding = DEFAULT_PADDING
|
val bottomPadding = DEFAULT_PADDING
|
||||||
AppBarTitle(
|
AppBarTitle(
|
||||||
@@ -671,9 +667,7 @@ fun ModalData.DetailedSMPStatsView(
|
|||||||
ModalView(
|
ModalView(
|
||||||
close = close
|
close = close
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
val bottomPadding = DEFAULT_PADDING
|
val bottomPadding = DEFAULT_PADDING
|
||||||
AppBarTitle(
|
AppBarTitle(
|
||||||
@@ -697,9 +691,7 @@ fun ModalData.XFTPServerSummaryView(
|
|||||||
ModalView(
|
ModalView(
|
||||||
close = close
|
close = close
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
val bottomPadding = DEFAULT_PADDING
|
val bottomPadding = DEFAULT_PADDING
|
||||||
AppBarTitle(
|
AppBarTitle(
|
||||||
@@ -715,9 +707,7 @@ fun ModalData.XFTPServerSummaryView(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) {
|
fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
var showUserSelection by remember { mutableStateOf(false) }
|
var showUserSelection by remember { mutableStateOf(false) }
|
||||||
val selectedUserCategory =
|
val selectedUserCategory =
|
||||||
remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } }
|
remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } }
|
||||||
|
|||||||
+55
-67
@@ -11,10 +11,13 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.themedBackground
|
||||||
|
import chat.simplex.common.views.chat.topPaddingToContent
|
||||||
import chat.simplex.common.views.newchat.ActiveProfilePicker
|
import chat.simplex.common.views.newchat.ActiveProfilePicker
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
|
||||||
@@ -22,26 +25,7 @@ import chat.simplex.res.MR
|
|||||||
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
Box(Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||||
Scaffold(
|
|
||||||
contentColor = LocalContentColor.current,
|
|
||||||
topBar = {
|
|
||||||
if (!oneHandUI.value) {
|
|
||||||
Column {
|
|
||||||
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bottomBar = {
|
|
||||||
if (oneHandUI.value) {
|
|
||||||
Column {
|
|
||||||
Divider()
|
|
||||||
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
val sharedContent = chatModel.sharedContent.value
|
val sharedContent = chatModel.sharedContent.value
|
||||||
var isMediaOrFileAttachment = false
|
var isMediaOrFileAttachment = false
|
||||||
var isVoice = false
|
var isVoice = false
|
||||||
@@ -69,22 +53,24 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
|||||||
}
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
Box(Modifier.padding(it)) {
|
if (chatModel.chats.value.isNotEmpty()) {
|
||||||
Column(
|
ShareList(
|
||||||
modifier = Modifier.fillMaxSize()
|
chatModel,
|
||||||
) {
|
search = searchInList,
|
||||||
if (chatModel.chats.value.isNotEmpty()) {
|
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||||
ShareList(
|
isVoice = isVoice,
|
||||||
chatModel,
|
hasSimplexLink = hasSimplexLink,
|
||||||
search = searchInList,
|
)
|
||||||
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
} else {
|
||||||
isVoice = isVoice,
|
EmptyList()
|
||||||
hasSimplexLink = hasSimplexLink,
|
}
|
||||||
)
|
if (oneHandUI.value) {
|
||||||
} else {
|
StatusBarBackground()
|
||||||
EmptyList()
|
} else {
|
||||||
}
|
NavigationBarBackground(oneHandUI.value, true)
|
||||||
}
|
}
|
||||||
|
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||||
|
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +94,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
|||||||
if (showSearch) {
|
if (showSearch) {
|
||||||
BackHandler(onBack = hideSearchOnBack)
|
BackHandler(onBack = hideSearchOnBack)
|
||||||
}
|
}
|
||||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
|
||||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||||
val navButton: @Composable RowScope.() -> Unit = {
|
val navButton: @Composable RowScope.() -> Unit = {
|
||||||
when {
|
when {
|
||||||
@@ -118,13 +103,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
|||||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||||
.all { u -> u.unreadCount == 0 }
|
.all { u -> u.unreadCount == 0 }
|
||||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||||
ModalManager.start.showCustomModal { close ->
|
ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
|
||||||
val search = rememberSaveable { mutableStateOf("") }
|
val search = rememberSaveable { mutableStateOf("") }
|
||||||
ModalView(
|
ModalView(
|
||||||
{ close() },
|
{ close() },
|
||||||
endButtons = {
|
showSearch = true,
|
||||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
searchAlwaysVisible = true,
|
||||||
},
|
onSearchValueChanged = { search.value = it },
|
||||||
content = {
|
content = {
|
||||||
ActiveProfilePicker(
|
ActiveProfilePicker(
|
||||||
search = search,
|
search = search,
|
||||||
@@ -148,31 +133,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chatModel.chats.value.size >= 8) {
|
|
||||||
barButtons.add {
|
|
||||||
IconButton({ showSearch = true }) {
|
|
||||||
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultTopAppBar(
|
DefaultAppBar(
|
||||||
navigationButton = navButton,
|
navigationButton = navButton,
|
||||||
title = {
|
title = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
@@ -191,8 +153,29 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
|||||||
},
|
},
|
||||||
onTitleClick = null,
|
onTitleClick = null,
|
||||||
showSearch = showSearch,
|
showSearch = showSearch,
|
||||||
|
onTop = !remember { appPrefs.oneHandUI.state }.value,
|
||||||
onSearchValueChanged = onSearchValueChanged,
|
onSearchValueChanged = onSearchValueChanged,
|
||||||
buttons = barButtons
|
buttons = {
|
||||||
|
if (chatModel.chats.value.size >= 8) {
|
||||||
|
IconButton({ showSearch = true }) {
|
||||||
|
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stopped) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,8 +194,13 @@ private fun ShareList(
|
|||||||
filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted)
|
filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val topPaddingToContent = topPaddingToContent()
|
||||||
LazyColumnWithScrollBar(
|
LazyColumnWithScrollBar(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
|
||||||
|
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||||
|
),
|
||||||
reverseLayout = oneHandUI.value
|
reverseLayout = oneHandUI.value
|
||||||
) {
|
) {
|
||||||
items(chats) { chat ->
|
items(chats) { chat ->
|
||||||
|
|||||||
+16
-6
@@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||||
import chat.simplex.common.model.ChatModel.controller
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
@@ -137,12 +138,16 @@ fun UserPicker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
val iconColor = MaterialTheme.colors.secondaryVariant
|
||||||
|
val background = if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface
|
||||||
PlatformUserPicker(
|
PlatformUserPicker(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier)
|
.then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true, ambientColor = background) else Modifier)
|
||||||
.background(if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface)
|
.padding(top = if (appPlatform.isDesktop && oneHandUI.value) 7.dp else 0.dp)
|
||||||
|
.background(background)
|
||||||
.padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
|
.padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
|
||||||
pickerState = userPickerState
|
pickerState = userPickerState
|
||||||
) {
|
) {
|
||||||
@@ -198,12 +203,13 @@ fun UserPicker(
|
|||||||
UserPickerUsersSection(
|
UserPickerUsersSection(
|
||||||
users = users,
|
users = users,
|
||||||
onUserClicked = onUserClicked,
|
onUserClicked = onUserClicked,
|
||||||
|
iconColor = iconColor,
|
||||||
stopped = stopped
|
stopped = stopped
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (currentUser != null) {
|
} else if (currentUser != null) {
|
||||||
SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
|
SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
|
||||||
ProfilePreview(currentUser.profile, stopped = stopped)
|
ProfilePreview(currentUser.profile, iconColor = iconColor, stopped = stopped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,6 +240,7 @@ fun UserPicker(
|
|||||||
Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) {
|
Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) {
|
||||||
UserPickerUsersSection(
|
UserPickerUsersSection(
|
||||||
users = inactiveUsers,
|
users = inactiveUsers,
|
||||||
|
iconColor = iconColor,
|
||||||
onUserClicked = onUserClicked,
|
onUserClicked = onUserClicked,
|
||||||
stopped = stopped
|
stopped = stopped
|
||||||
)
|
)
|
||||||
@@ -265,13 +272,15 @@ fun UserPicker(
|
|||||||
generalGetString(MR.strings.auth_open_chat_profiles),
|
generalGetString(MR.strings.auth_open_chat_profiles),
|
||||||
generalGetString(MR.strings.auth_log_in_using_credential)
|
generalGetString(MR.strings.auth_log_in_using_credential)
|
||||||
) {
|
) {
|
||||||
ModalManager.start.showCustomModal { close ->
|
ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
|
||||||
val search = rememberSaveable { mutableStateOf("") }
|
val search = rememberSaveable { mutableStateOf("") }
|
||||||
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
||||||
ModalView(
|
ModalView(
|
||||||
{ close() },
|
{ close() },
|
||||||
endButtons = {
|
showSearch = true,
|
||||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
searchAlwaysVisible = true,
|
||||||
|
onSearchValueChanged = {
|
||||||
|
search.value = it
|
||||||
},
|
},
|
||||||
content = { UserProfilesView(chatModel, search, profileHidden) })
|
content = { UserProfilesView(chatModel, search, profileHidden) })
|
||||||
}
|
}
|
||||||
@@ -519,6 +528,7 @@ private fun DevicePickerRow(
|
|||||||
@Composable
|
@Composable
|
||||||
expect fun UserPickerUsersSection(
|
expect fun UserPickerUsersSection(
|
||||||
users: List<UserInfo>,
|
users: List<UserInfo>,
|
||||||
|
iconColor: Color,
|
||||||
stopped: Boolean,
|
stopped: Boolean,
|
||||||
onUserClicked: (user: User) -> Unit,
|
onUserClicked: (user: User) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-3
@@ -46,9 +46,7 @@ fun ChatArchiveLayout(
|
|||||||
saveArchive: () -> Unit,
|
saveArchive: () -> Unit,
|
||||||
deleteArchiveAlert: () -> Unit
|
deleteArchiveAlert: () -> Unit
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
AppBarTitle(title)
|
AppBarTitle(title)
|
||||||
SectionView(stringResource(MR.strings.chat_archive_section)) {
|
SectionView(stringResource(MR.strings.chat_archive_section)) {
|
||||||
SettingsActionItem(
|
SettingsActionItem(
|
||||||
|
|||||||
+1
-1
@@ -203,7 +203,7 @@ fun DatabaseEncryptionLayout(
|
|||||||
Layout()
|
Layout()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) {
|
ColumnWithScrollBar(maxIntrinsicSize = true) {
|
||||||
Layout()
|
Layout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -77,10 +77,7 @@ fun DatabaseErrorView(
|
|||||||
Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", ")))
|
Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBarNoAppBar(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
|
||||||
Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||||
when (val status = chatDbStatus.value) {
|
when (val status = chatDbStatus.value) {
|
||||||
is DBMigrationResult.ErrorNotADatabase ->
|
is DBMigrationResult.ErrorNotADatabase ->
|
||||||
|
|||||||
+1
-3
@@ -156,9 +156,7 @@ fun DatabaseLayout(
|
|||||||
val stopped = !runChat
|
val stopped = !runChat
|
||||||
val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote
|
val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote
|
||||||
|
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar {
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
AppBarTitle(stringResource(MR.strings.your_chat_database))
|
AppBarTitle(stringResource(MR.strings.your_chat_database))
|
||||||
|
|
||||||
if (!chatModel.desktopNoUserNoRemote) {
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.chatlist.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
|
||||||
|
val handler = LocalAppBarHandler.current
|
||||||
|
val connection = handler?.connection
|
||||||
|
LaunchedEffect(title) {
|
||||||
|
handler?.title?.value = title
|
||||||
|
}
|
||||||
|
val theme = CurrentColors.collectAsState()
|
||||||
|
val titleColor = MaterialTheme.appColors.title
|
||||||
|
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
|
||||||
|
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||||
|
else // color is not updated when changing themes if I pass null here
|
||||||
|
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
Modifier
|
||||||
|
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = bottomTitleAlpha(connection)
|
||||||
|
},
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.h1.copy(brush = brush),
|
||||||
|
color = MaterialTheme.colors.primaryVariant,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
if (hostDevice != null) {
|
||||||
|
Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer {
|
||||||
|
alpha = bottomTitleAlpha(connection)
|
||||||
|
}) {
|
||||||
|
HostDeviceTitle(hostDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(bottomPadding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) =
|
||||||
|
if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
|
||||||
|
else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
|
||||||
|
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
||||||
|
DevicePill(
|
||||||
|
active = true,
|
||||||
|
onClick = {},
|
||||||
|
actionButtonVisible = false,
|
||||||
|
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
|
||||||
|
text = hostDevice.second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import chat.simplex.common.platform.appPlatform
|
||||||
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
|
|
||||||
|
fun Modifier.blurredBackgroundModifier(
|
||||||
|
keyboardInset: WindowInsets,
|
||||||
|
handler: AppBarHandler?,
|
||||||
|
blurRadius: State<Int>,
|
||||||
|
prefAlpha: State<Float>,
|
||||||
|
keyboardCoversBar: Boolean,
|
||||||
|
onTop: Boolean,
|
||||||
|
density: Density
|
||||||
|
): Modifier {
|
||||||
|
val graphicsLayer = handler?.graphicsLayer
|
||||||
|
val backgroundGraphicsLayer = handler?.backgroundGraphicsLayer
|
||||||
|
val backgroundGraphicsLayerSize = handler?.backgroundGraphicsLayerSize
|
||||||
|
if (handler == null || graphicsLayer == null || backgroundGraphicsLayer == null || blurRadius.value == 0 || prefAlpha.value == 1f || backgroundGraphicsLayerSize === null)
|
||||||
|
return this
|
||||||
|
|
||||||
|
return if (appPlatform.isAndroid) {
|
||||||
|
this.androidBlurredModifier(keyboardInset, blurRadius.value, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
|
||||||
|
} else {
|
||||||
|
this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is more performant version than for Android but can't be used on desktop because on first frame it shows transparent view
|
||||||
|
// which is very noticeable on desktop and unnoticeable on Android
|
||||||
|
private fun Modifier.androidBlurredModifier(
|
||||||
|
keyboardInset: WindowInsets,
|
||||||
|
blurRadius: Int,
|
||||||
|
keyboardCoversBar: Boolean,
|
||||||
|
onTop: Boolean,
|
||||||
|
graphicsLayer: GraphicsLayer,
|
||||||
|
backgroundGraphicsLayer: GraphicsLayer,
|
||||||
|
backgroundGraphicsLayerSize: State<IntSize>,
|
||||||
|
density: Density
|
||||||
|
): Modifier = this
|
||||||
|
.graphicsLayer {
|
||||||
|
renderEffect = if (blurRadius > 0) BlurEffect(blurRadius.dp.toPx(), blurRadius.dp.toPx()) else null
|
||||||
|
clip = blurRadius > 0
|
||||||
|
}
|
||||||
|
.graphicsLayer {
|
||||||
|
if (!onTop) {
|
||||||
|
val bgSize = when {
|
||||||
|
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||||
|
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||||
|
else -> backgroundGraphicsLayerSize.value.height
|
||||||
|
}
|
||||||
|
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||||
|
translationY = -bgSize + size.height + keyboardHeightCovered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(Color.Black)
|
||||||
|
if (onTop) {
|
||||||
|
clipRect {
|
||||||
|
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||||
|
drawLayer(backgroundGraphicsLayer)
|
||||||
|
} else {
|
||||||
|
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||||
|
}
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||||
|
drawLayer(backgroundGraphicsLayer)
|
||||||
|
} else {
|
||||||
|
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||||
|
}
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphicsLayer {
|
||||||
|
if (!onTop) {
|
||||||
|
val bgSize = when {
|
||||||
|
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||||
|
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||||
|
else -> backgroundGraphicsLayerSize.value.height
|
||||||
|
}
|
||||||
|
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||||
|
translationY -= -bgSize + size.height + keyboardHeightCovered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.desktopBlurredModifier(
|
||||||
|
keyboardInset: WindowInsets,
|
||||||
|
blurRadius: State<Int>,
|
||||||
|
keyboardCoversBar: Boolean,
|
||||||
|
onTop: Boolean,
|
||||||
|
graphicsLayer: GraphicsLayer,
|
||||||
|
backgroundGraphicsLayer: GraphicsLayer,
|
||||||
|
backgroundGraphicsLayerSize: State<IntSize>,
|
||||||
|
density: Density
|
||||||
|
): Modifier = this
|
||||||
|
.graphicsLayer {
|
||||||
|
renderEffect = if (blurRadius.value > 0) BlurEffect(blurRadius.value.dp.toPx(), blurRadius.value.dp.toPx()) else null
|
||||||
|
clip = blurRadius.value > 0
|
||||||
|
}
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(Color.Black)
|
||||||
|
if (onTop) {
|
||||||
|
clipRect {
|
||||||
|
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||||
|
drawLayer(backgroundGraphicsLayer)
|
||||||
|
} else {
|
||||||
|
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||||
|
}
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val bgSize = when {
|
||||||
|
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||||
|
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||||
|
else -> backgroundGraphicsLayerSize.value.height
|
||||||
|
}
|
||||||
|
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||||
|
translate(top = -bgSize + size.height + keyboardHeightCovered) {
|
||||||
|
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||||
|
drawLayer(backgroundGraphicsLayer)
|
||||||
|
} else {
|
||||||
|
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||||
|
}
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
-41
@@ -1,11 +1,13 @@
|
|||||||
package chat.simplex.common.views.helpers
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.draw.CacheDrawScope
|
import androidx.compose.ui.draw.CacheDrawScope
|
||||||
import androidx.compose.ui.draw.DrawResult
|
import androidx.compose.ui.draw.DrawResult
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.graphics.drawscope.*
|
import androidx.compose.ui.graphics.drawscope.*
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
@@ -381,7 +383,14 @@ private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, siz
|
|||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult {
|
fun CacheDrawScope.chatViewBackground(
|
||||||
|
image: ImageBitmap,
|
||||||
|
imageType: WallpaperType,
|
||||||
|
background: Color,
|
||||||
|
tint: Color,
|
||||||
|
graphicsLayerSize: MutableState<IntSize>? = null,
|
||||||
|
backgroundGraphicsLayer: GraphicsLayer? = null
|
||||||
|
): DrawResult {
|
||||||
val imageScale = if (imageType is WallpaperType.Preset) {
|
val imageScale = if (imageType is WallpaperType.Preset) {
|
||||||
(imageType.scale ?: 1f) * imageType.predefinedImageScale
|
(imageType.scale ?: 1f) * imageType.predefinedImageScale
|
||||||
} else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) {
|
} else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) {
|
||||||
@@ -396,53 +405,55 @@ fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperTy
|
|||||||
}
|
}
|
||||||
|
|
||||||
return onDrawBehind {
|
return onDrawBehind {
|
||||||
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
|
copyBackgroundToAppBar(graphicsLayerSize, backgroundGraphicsLayer) {
|
||||||
drawRect(background)
|
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
|
||||||
when (imageType) {
|
drawRect(background)
|
||||||
is WallpaperType.Preset -> drawImage(image)
|
when (imageType) {
|
||||||
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
|
is WallpaperType.Preset -> drawImage(image)
|
||||||
WallpaperScaleType.REPEAT -> drawImage(image)
|
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
|
||||||
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
|
WallpaperScaleType.REPEAT -> drawImage(image)
|
||||||
clipRect {
|
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
|
||||||
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
clipRect {
|
||||||
val scaledWidth = (image.width * scale.scaleX).roundToInt()
|
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||||
val scaledHeight = (image.height * scale.scaleY).roundToInt()
|
val scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||||
// Large image will cause freeze
|
val scaledHeight = (image.height * scale.scaleY).roundToInt()
|
||||||
if (image.width > 4320 || image.height > 4320) return@clipRect
|
// Large image will cause freeze
|
||||||
|
if (image.width > 4320 || image.height > 4320) return@clipRect
|
||||||
|
|
||||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
if (scaleType == WallpaperScaleType.FIT) {
|
if (scaleType == WallpaperScaleType.FIT) {
|
||||||
if (scaledWidth < size.width) {
|
if (scaledWidth < size.width) {
|
||||||
// has black lines at left and right sides
|
// has black lines at left and right sides
|
||||||
var x = (size.width - scaledWidth) / 2
|
var x = (size.width - scaledWidth) / 2
|
||||||
while (x > 0) {
|
while (x > 0) {
|
||||||
drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
x -= scaledWidth
|
x -= scaledWidth
|
||||||
}
|
}
|
||||||
x = size.width - (size.width - scaledWidth) / 2
|
x = size.width - (size.width - scaledWidth) / 2
|
||||||
while (x < size.width) {
|
while (x < size.width) {
|
||||||
drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
x += scaledWidth
|
x += scaledWidth
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// has black lines at top and bottom sides
|
// has black lines at top and bottom sides
|
||||||
var y = (size.height - scaledHeight) / 2
|
var y = (size.height - scaledHeight) / 2
|
||||||
while (y > 0) {
|
while (y > 0) {
|
||||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
y -= scaledHeight
|
y -= scaledHeight
|
||||||
}
|
}
|
||||||
y = size.height - (size.height - scaledHeight) / 2
|
y = size.height - (size.height - scaledHeight) / 2
|
||||||
while (y < size.height) {
|
while (y < size.height) {
|
||||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
y += scaledHeight
|
y += scaledHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
drawRect(tint)
|
||||||
}
|
}
|
||||||
drawRect(tint)
|
|
||||||
}
|
}
|
||||||
|
is WallpaperType.Empty -> {}
|
||||||
}
|
}
|
||||||
is WallpaperType.Empty -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -19,6 +19,8 @@ fun ChooseAttachmentView(attachmentOption: MutableState<AttachmentOption?>, hide
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.imePadding()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
if (!focusState.hasFocus) hide()
|
if (!focusState.hasFocus) hide()
|
||||||
|
|||||||
-181
@@ -1,181 +0,0 @@
|
|||||||
package chat.simplex.common.views.helpers
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.ui.draw.*
|
|
||||||
import androidx.compose.ui.graphics.*
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.*
|
|
||||||
import chat.simplex.common.platform.appPlatform
|
|
||||||
import chat.simplex.common.ui.theme.*
|
|
||||||
import chat.simplex.common.views.chatlist.DevicePill
|
|
||||||
import chat.simplex.res.MR
|
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, barPaddingValues: PaddingValues = PaddingValues(horizontal = AppBarHorizontalPadding), endButtons: @Composable RowScope.() -> Unit = {}) {
|
|
||||||
var rowModifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
|
||||||
val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
|
||||||
if (!closeBarTitle.isNullOrEmpty()) {
|
|
||||||
rowModifier = rowModifier.background(themeBackgroundMix)
|
|
||||||
}
|
|
||||||
val handler = LocalAppBarHandler.current
|
|
||||||
val connection = LocalAppBarHandler.current?.connection
|
|
||||||
val title = remember(handler?.title?.value) { handler?.title ?: mutableStateOf("") }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = arrangement,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
|
|
||||||
.drawWithCache {
|
|
||||||
val backgroundColor = if (appPlatform.isDesktop && connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(connection)) else Color.Transparent
|
|
||||||
onDrawBehind {
|
|
||||||
if (appPlatform.isDesktop) {
|
|
||||||
drawRect(backgroundColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(barPaddingValues),
|
|
||||||
content = {
|
|
||||||
Row(
|
|
||||||
rowModifier,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
if (showClose) {
|
|
||||||
NavigationButtonBack(tintColor = tintColor, onButtonClicked = close)
|
|
||||||
} else {
|
|
||||||
Spacer(Modifier)
|
|
||||||
}
|
|
||||||
if (!closeBarTitle.isNullOrEmpty()) {
|
|
||||||
Row(
|
|
||||||
Modifier.weight(1f),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
closeBarTitle,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (title.value.isNotEmpty() && connection != null) {
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.padding(start = if (showClose) 0.dp else DEFAULT_PADDING_HALF)
|
|
||||||
.weight(1f) // hides the title if something wants full width (eg, search field in chat profiles screen)
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = topTitleAlpha((connection))
|
|
||||||
}
|
|
||||||
.padding(start = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
title.value,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
endButtons()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (closeBarTitle.isNullOrEmpty() && title.value.isNotEmpty() && connection != null) {
|
|
||||||
Divider(
|
|
||||||
Modifier
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = topTitleAlpha(connection)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
|
|
||||||
val handler = LocalAppBarHandler.current
|
|
||||||
val connection = handler?.connection
|
|
||||||
LaunchedEffect(title) {
|
|
||||||
handler?.title?.value = title
|
|
||||||
}
|
|
||||||
val theme = CurrentColors.collectAsState()
|
|
||||||
val titleColor = MaterialTheme.appColors.title
|
|
||||||
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
|
|
||||||
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
|
||||||
else // color is not updated when changing themes if I pass null here
|
|
||||||
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
Modifier
|
|
||||||
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,)
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = bottomTitleAlpha(connection)
|
|
||||||
},
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.h1.copy(brush = brush),
|
|
||||||
color = MaterialTheme.colors.primaryVariant,
|
|
||||||
textAlign = TextAlign.Start
|
|
||||||
)
|
|
||||||
if (hostDevice != null) {
|
|
||||||
Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer {
|
|
||||||
alpha = bottomTitleAlpha(connection)
|
|
||||||
}) {
|
|
||||||
HostDeviceTitle(hostDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(bottomPadding))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun topTitleAlpha(connection: CollapsingAppBarNestedScrollConnection) =
|
|
||||||
if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
|
|
||||||
else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, 1f)
|
|
||||||
|
|
||||||
private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) =
|
|
||||||
if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
|
|
||||||
else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
|
|
||||||
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
|
||||||
DevicePill(
|
|
||||||
active = true,
|
|
||||||
onClick = {},
|
|
||||||
actionButtonVisible = false,
|
|
||||||
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
|
|
||||||
text = hostDevice.second
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview/*(
|
|
||||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
|
||||||
showBackground = true,
|
|
||||||
name = "Dark Mode"
|
|
||||||
)*/
|
|
||||||
@Composable
|
|
||||||
fun PreviewCloseSheetBar() {
|
|
||||||
SimpleXTheme {
|
|
||||||
CloseSheetBar(close = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+55
-1
@@ -3,15 +3,67 @@ package chat.simplex.common.views.helpers
|
|||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.*
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
|
||||||
val LocalAppBarHandler: ProvidableCompositionLocal<AppBarHandler?> = staticCompositionLocalOf { null }
|
val LocalAppBarHandler: ProvidableCompositionLocal<AppBarHandler?> = staticCompositionLocalOf { null }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberAppBarHandler(key1: Any? = null, key2: Any? = null, keyboardCoversBar: Boolean = true): AppBarHandler {
|
||||||
|
val graphicsLayer = rememberGraphicsLayer()
|
||||||
|
val backgroundGraphicsLayer = rememberGraphicsLayer()
|
||||||
|
return remember(key1, key2) { AppBarHandler(graphicsLayer, backgroundGraphicsLayer, keyboardCoversBar) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun adjustAppBarHandler(handler: AppBarHandler): AppBarHandler {
|
||||||
|
val graphicsLayer = rememberGraphicsLayer()
|
||||||
|
val backgroundGraphicsLayer = rememberGraphicsLayer()
|
||||||
|
if (handler.graphicsLayer == null || handler.graphicsLayer?.isReleased == true || handler.backgroundGraphicsLayer?.isReleased == true) {
|
||||||
|
handler.graphicsLayer = graphicsLayer
|
||||||
|
handler.backgroundGraphicsLayer = backgroundGraphicsLayer
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.copyViewToAppBar(blurRadius: Int, graphicsLayer: GraphicsLayer?): Modifier {
|
||||||
|
return if (blurRadius > 0 && graphicsLayer != null) {
|
||||||
|
this.drawWithContent {
|
||||||
|
graphicsLayer.record {
|
||||||
|
this@drawWithContent.drawContent()
|
||||||
|
}
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
} else this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DrawScope.copyBackgroundToAppBar(graphicsLayerSize: MutableState<IntSize>?, backgroundGraphicsLayer: GraphicsLayer?, scope: DrawScope.() -> Unit) {
|
||||||
|
val blurRadius = appPrefs.appearanceBarsBlurRadius.get()
|
||||||
|
if (blurRadius > 0 && graphicsLayerSize != null && backgroundGraphicsLayer != null) {
|
||||||
|
graphicsLayerSize.value = backgroundGraphicsLayer.size
|
||||||
|
backgroundGraphicsLayer.record {
|
||||||
|
scope()
|
||||||
|
}
|
||||||
|
drawLayer(backgroundGraphicsLayer)
|
||||||
|
} else {
|
||||||
|
scope()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AppBarHandler(
|
class AppBarHandler(
|
||||||
|
var graphicsLayer: GraphicsLayer?,
|
||||||
|
var backgroundGraphicsLayer: GraphicsLayer?,
|
||||||
|
val keyboardCoversBar: Boolean = true,
|
||||||
listState: LazyListState = LazyListState(0, 0),
|
listState: LazyListState = LazyListState(0, 0),
|
||||||
scrollState: ScrollState = ScrollState(initial = 0)
|
scrollState: ScrollState = ScrollState(initial = 0)
|
||||||
) {
|
) {
|
||||||
@@ -24,6 +76,8 @@ class AppBarHandler(
|
|||||||
|
|
||||||
val connection = CollapsingAppBarNestedScrollConnection()
|
val connection = CollapsingAppBarNestedScrollConnection()
|
||||||
|
|
||||||
|
val backgroundGraphicsLayerSize: MutableState<IntSize> = mutableStateOf(IntSize.Zero)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var appBarMaxHeightPx: Int = 0
|
var appBarMaxHeightPx: Int = 0
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -3,7 +3,6 @@ package chat.simplex.common.views.helpers
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||||
import androidx.compose.foundation.text.*
|
import androidx.compose.foundation.text.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@@ -22,13 +21,11 @@ import androidx.compose.ui.text.input.*
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.views.database.PassphraseStrength
|
import chat.simplex.common.views.database.PassphraseStrength
|
||||||
import chat.simplex.common.views.database.validKey
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultBasicTextField(
|
fun DefaultBasicTextField(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
|||||||
+2
-1
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
@Composable
|
@Composable
|
||||||
fun DefaultDropdownMenu(
|
fun DefaultDropdownMenu(
|
||||||
showMenu: MutableState<Boolean>,
|
showMenu: MutableState<Boolean>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
offset: DpOffset = DpOffset(0.dp, 0.dp),
|
offset: DpOffset = DpOffset(0.dp, 0.dp),
|
||||||
dropdownMenuItems: (@Composable () -> Unit)?
|
dropdownMenuItems: (@Composable () -> Unit)?
|
||||||
) {
|
) {
|
||||||
@@ -23,7 +24,7 @@ fun DefaultDropdownMenu(
|
|||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showMenu.value,
|
expanded = showMenu.value,
|
||||||
onDismissRequest = { showMenu.value = false },
|
onDismissRequest = { showMenu.value = false },
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.widthIn(min = 250.dp)
|
.widthIn(min = 250.dp)
|
||||||
.background(MaterialTheme.colors.surface)
|
.background(MaterialTheme.colors.surface)
|
||||||
.padding(vertical = 4.dp),
|
.padding(vertical = 4.dp),
|
||||||
|
|||||||
+184
-59
@@ -3,44 +3,120 @@ package chat.simplex.common.views.helpers
|
|||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.*
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.chat.item.CenteredRowLayout
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultTopAppBar(
|
fun DefaultAppBar(
|
||||||
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||||
title: (@Composable () -> Unit)?,
|
title: (@Composable () -> Unit)? = null,
|
||||||
|
fixedTitleText: String? = null,
|
||||||
onTitleClick: (() -> Unit)? = null,
|
onTitleClick: (() -> Unit)? = null,
|
||||||
showSearch: Boolean,
|
onTop: Boolean,
|
||||||
onSearchValueChanged: (String) -> Unit,
|
showSearch: Boolean = false,
|
||||||
buttons: List<@Composable RowScope.() -> Unit> = emptyList(),
|
searchAlwaysVisible: Boolean = false,
|
||||||
|
onSearchValueChanged: (String) -> Unit = {},
|
||||||
|
buttons: @Composable RowScope.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
// If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier
|
// If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier
|
||||||
val modifier = if (!showSearch) {
|
val modifier = if (!showSearch) {
|
||||||
Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { })
|
Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { })
|
||||||
} else Modifier
|
} else Modifier.imePadding()
|
||||||
|
|
||||||
TopAppBar(
|
val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||||
modifier = modifier,
|
val prefAlpha = remember { appPrefs.inAppBarsAlpha.state }
|
||||||
title = {
|
val handler = LocalAppBarHandler.current
|
||||||
if (!showSearch) {
|
val connection = LocalAppBarHandler.current?.connection
|
||||||
title?.invoke()
|
val titleText = remember(handler?.title?.value, fixedTitleText) {
|
||||||
} else {
|
if (fixedTitleText != null) {
|
||||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged)
|
mutableStateOf(fixedTitleText)
|
||||||
|
} else {
|
||||||
|
handler?.title ?: mutableStateOf("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val keyboardInset = WindowInsets.ime
|
||||||
|
Box(modifier) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val blurRadius = remember { appPrefs.appearanceBarsBlurRadius.state }
|
||||||
|
Box(Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.blurredBackgroundModifier(keyboardInset, handler, blurRadius, prefAlpha, handler?.keyboardCoversBar == true, onTop, density)
|
||||||
|
.drawWithCache {
|
||||||
|
// store it as a variable, don't put it inside if without holding it here. Compiler don't see it changes otherwise
|
||||||
|
val alpha = prefAlpha.value
|
||||||
|
val backgroundColor = if (title != null || fixedTitleText != null || connection == null || !onTop) {
|
||||||
|
themeBackgroundMix.copy(alpha)
|
||||||
|
} else {
|
||||||
|
themeBackgroundMix.copy(topTitleAlpha(false, connection))
|
||||||
|
}
|
||||||
|
onDrawBehind {
|
||||||
|
drawRect(backgroundColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f),
|
Box(
|
||||||
navigationIcon = navigationButton,
|
Modifier
|
||||||
buttons = if (!showSearch) buttons else emptyList(),
|
.fillMaxWidth()
|
||||||
centered = !showSearch,
|
.then(if (!onTop) Modifier.navigationBarsPadding() else Modifier)
|
||||||
|
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
|
||||||
|
) {
|
||||||
|
AppBar(
|
||||||
|
title = {
|
||||||
|
if (showSearch) {
|
||||||
|
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged)
|
||||||
|
} else if (title != null) {
|
||||||
|
title()
|
||||||
|
} else if (titleText.value.isNotEmpty() && connection != null) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = if (fixedTitleText != null) 1f else topTitleAlpha(true, connection)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
titleText.value,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = navigationButton,
|
||||||
|
buttons = if (!showSearch) buttons else {{}},
|
||||||
|
centered = !showSearch && (title != null || !onTop),
|
||||||
|
onTop = onTop,
|
||||||
|
)
|
||||||
|
AppBarDivider(onTop, title != null || fixedTitleText != null, connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CallAppBar(
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
AppBar(
|
||||||
|
title,
|
||||||
|
navigationIcon = { NavigationButtonBack(tintColor = Color(0xFFFFFFD8), onButtonClicked = onBack) },
|
||||||
|
centered = false,
|
||||||
|
onTop = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,58 +159,107 @@ fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopAppBar(
|
private fun BoxScope.AppBarDivider(onTop: Boolean, fixedAlpha: Boolean, connection: CollapsingAppBarNestedScrollConnection?) {
|
||||||
|
if (connection != null) {
|
||||||
|
Divider(
|
||||||
|
Modifier
|
||||||
|
.align(if (onTop) Alignment.BottomStart else Alignment.TopStart)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = if (!onTop || fixedAlpha) 1f else topTitleAlpha(false, connection, 1f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Divider(Modifier.align(if (onTop) Alignment.BottomStart else Alignment.TopStart))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppBar(
|
||||||
title: @Composable () -> Unit,
|
title: @Composable () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
|
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
|
||||||
buttons: List<@Composable RowScope.() -> Unit> = emptyList(),
|
buttons: @Composable RowScope.() -> Unit = {},
|
||||||
backgroundColor: Color = MaterialTheme.colors.primarySurface,
|
|
||||||
centered: Boolean,
|
centered: Boolean,
|
||||||
|
onTop: Boolean,
|
||||||
) {
|
) {
|
||||||
Box(
|
val adjustedModifier = modifier
|
||||||
modifier
|
.then(if (onTop) Modifier.statusBarsPadding() else Modifier)
|
||||||
.fillMaxWidth()
|
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
.fillMaxWidth()
|
||||||
.background(backgroundColor)
|
.padding(horizontal = AppBarHorizontalPadding)
|
||||||
.padding(horizontal = 4.dp),
|
if (centered) {
|
||||||
contentAlignment = Alignment.CenterStart,
|
AppBarCenterAligned(adjustedModifier, title, navigationIcon, buttons)
|
||||||
|
} else {
|
||||||
|
AppBarStartAligned(adjustedModifier, title, navigationIcon, buttons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppBarStartAligned(
|
||||||
|
modifier: Modifier,
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
buttons: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (navigationIcon != null) {
|
if (navigationIcon != null) {
|
||||||
Row(
|
navigationIcon()
|
||||||
Modifier
|
Spacer(Modifier.width(AppBarHorizontalPadding))
|
||||||
.fillMaxHeight()
|
} else {
|
||||||
.width(TitleInsetWithIcon - AppBarHorizontalPadding),
|
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
}
|
||||||
content = navigationIcon
|
Row(Modifier
|
||||||
)
|
.weight(1f)
|
||||||
|
.padding(end = DEFAULT_PADDING_HALF)
|
||||||
|
) {
|
||||||
|
title()
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
buttons.forEach { it() }
|
buttons()
|
||||||
}
|
|
||||||
val startPadding = if (navigationIcon != null) TitleInsetWithIcon else TitleInsetWithoutIcon
|
|
||||||
val endPadding = (buttons.size * 50f).dp
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
start = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else startPadding,
|
|
||||||
end = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else endPadding
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
title()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppBarCenterAligned(
|
||||||
|
modifier: Modifier,
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
buttons: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
CenteredRowLayout(modifier) {
|
||||||
|
if (navigationIcon != null) {
|
||||||
|
Row(
|
||||||
|
Modifier.padding(end = AppBarHorizontalPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = navigationIcon
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
Modifier.padding(end = DEFAULT_PADDING_HALF)
|
||||||
|
) {
|
||||||
|
title()
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
buttons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) =
|
||||||
|
if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
|
||||||
|
else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha)
|
||||||
|
|
||||||
val AppBarHeight = 56.dp
|
val AppBarHeight = 56.dp
|
||||||
val AppBarHorizontalPadding = 4.dp
|
val AppBarHorizontalPadding = 2.dp
|
||||||
val BottomAppBarHeight = 60.dp
|
|
||||||
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
|
|
||||||
val TitleInsetWithIcon = 72.dp
|
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -107,6 +106,7 @@ fun <T> ExposedDropDownSettingWithIcon(
|
|||||||
expanded.value = !expanded.value && enabled.value
|
expanded.value = !expanded.value && enabled.value
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
val ripple = remember { ripple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)) }
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.background(background, CircleShape)
|
.background(background, CircleShape)
|
||||||
@@ -115,7 +115,7 @@ fun <T> ExposedDropDownSettingWithIcon(
|
|||||||
onClick = {},
|
onClick = {},
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)),
|
indication = ripple,
|
||||||
enabled = enabled.value
|
enabled = enabled.value
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
|||||||
+36
-24
@@ -6,12 +6,14 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.chatlist.StatusBarBackground
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -21,24 +23,40 @@ import kotlin.math.sqrt
|
|||||||
fun ModalView(
|
fun ModalView(
|
||||||
close: () -> Unit,
|
close: () -> Unit,
|
||||||
showClose: Boolean = true,
|
showClose: Boolean = true,
|
||||||
|
showAppBar: Boolean = true,
|
||||||
enableClose: Boolean = true,
|
enableClose: Boolean = true,
|
||||||
background: Color = MaterialTheme.colors.background,
|
background: Color = Color.Unspecified,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
closeOnTop: Boolean = true,
|
showSearch: Boolean = false,
|
||||||
|
searchAlwaysVisible: Boolean = false,
|
||||||
|
onSearchValueChanged: (String) -> Unit = {},
|
||||||
endButtons: @Composable RowScope.() -> Unit = {},
|
endButtons: @Composable RowScope.() -> Unit = {},
|
||||||
content: @Composable () -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
if (showClose) {
|
if (showClose && showAppBar) {
|
||||||
BackHandler(enabled = enableClose, onBack = close)
|
BackHandler(enabled = enableClose, onBack = close)
|
||||||
}
|
}
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||||
if (closeOnTop) {
|
|
||||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
|
||||||
}
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
if (showAppBar) {
|
||||||
|
if (oneHandUI.value) {
|
||||||
|
StatusBarBackground()
|
||||||
|
}
|
||||||
|
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||||
|
DefaultAppBar(
|
||||||
|
navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null,
|
||||||
|
onTop = !oneHandUI.value,
|
||||||
|
showSearch = showSearch,
|
||||||
|
searchAlwaysVisible = searchAlwaysVisible,
|
||||||
|
onSearchValueChanged = onSearchValueChanged,
|
||||||
|
buttons = endButtons
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +65,7 @@ enum class ModalPlacement {
|
|||||||
START, CENTER, END, FULLSCREEN
|
START, CENTER, END, FULLSCREEN
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModalData() {
|
class ModalData(val keyboardCoversBar: Boolean = true) {
|
||||||
private val state = mutableMapOf<String, MutableState<Any?>>()
|
private val state = mutableMapOf<String, MutableState<Any?>>()
|
||||||
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
|
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
|
||||||
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
|
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
|
||||||
@@ -55,7 +73,7 @@ class ModalData() {
|
|||||||
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> =
|
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> =
|
||||||
state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?>
|
state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?>
|
||||||
|
|
||||||
val appBarHandler = AppBarHandler()
|
val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||||
@@ -69,23 +87,21 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||||
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||||
|
|
||||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||||
val data = ModalData()
|
|
||||||
showCustomModal { close ->
|
showCustomModal { close ->
|
||||||
ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() })
|
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||||
val data = ModalData()
|
|
||||||
showCustomModal { close ->
|
showCustomModal { close ->
|
||||||
ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) })
|
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCustomModal(animated: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||||
Log.d(TAG, "ModalManager.showCustomModal")
|
Log.d(TAG, "ModalManager.showCustomModal")
|
||||||
val data = ModalData()
|
val data = ModalData(keyboardCoversBar = keyboardCoversBar)
|
||||||
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
||||||
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
|
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
|
||||||
if (toRemove.isNotEmpty()) {
|
if (toRemove.isNotEmpty()) {
|
||||||
@@ -146,9 +162,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
// Without animation
|
// Without animation
|
||||||
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
|
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
|
||||||
modalViews.lastOrNull()?.let {
|
modalViews.lastOrNull()?.let {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||||
LocalAppBarHandler provides it.second.appBarHandler
|
|
||||||
) {
|
|
||||||
it.third(it.second, ::closeModal)
|
it.third(it.second, ::closeModal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,9 +178,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
modalViews.getOrNull(it - 1)?.let {
|
modalViews.getOrNull(it - 1)?.let {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||||
LocalAppBarHandler provides it.second.appBarHandler
|
|
||||||
) {
|
|
||||||
it.third(it.second, ::closeModal)
|
it.third(it.second, ::closeModal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user