mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6b7d8353c | |||
| 113f0cce6a | |||
| ffd0de57d5 | |||
| b24d4980b6 | |||
| 141f3c8fe5 |
@@ -27,7 +27,6 @@ struct ContentView: View {
|
|||||||
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
||||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
|
||||||
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
||||||
@State private var showWhatsNew = false
|
@State private var showWhatsNew = false
|
||||||
@State private var showChooseLAMode = false
|
@State private var showChooseLAMode = false
|
||||||
@@ -250,7 +249,7 @@ 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)
|
||||||
.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
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ struct ChatView: View {
|
|||||||
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
|
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
|
||||||
|
|
||||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||||
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
@@ -116,6 +117,7 @@ struct ChatView: View {
|
|||||||
.background(theme.colors.background)
|
.background(theme.colors.background)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.environmentObject(theme)
|
.environmentObject(theme)
|
||||||
|
.privacySensitive(protectScreen)
|
||||||
.confirmationDialog(selectedChatItems?.count == 1 ? "Delete message?" : "Delete \((selectedChatItems?.count ?? 0)) messages?", isPresented: $showDeleteSelectedMessages, titleVisibility: .visible) {
|
.confirmationDialog(selectedChatItems?.count == 1 ? "Delete message?" : "Delete \((selectedChatItems?.count ?? 0)) messages?", isPresented: $showDeleteSelectedMessages, titleVisibility: .visible) {
|
||||||
Button("Delete for me", role: .destructive) {
|
Button("Delete for me", role: .destructive) {
|
||||||
if let selected = selectedChatItems {
|
if let selected = selectedChatItems {
|
||||||
@@ -211,126 +213,130 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
if selectedChatItems != nil {
|
Group {
|
||||||
SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
|
if selectedChatItems != nil {
|
||||||
} else if case let .direct(contact) = cInfo {
|
SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
|
||||||
Button {
|
} else if case let .direct(contact) = cInfo {
|
||||||
Task {
|
Button {
|
||||||
showChatInfoSheet = true
|
Task {
|
||||||
|
showChatInfoSheet = true
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
ChatInfoToolbar(chat: chat)
|
||||||
}
|
}
|
||||||
} label: {
|
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
||||||
|
ChatInfoView(
|
||||||
|
chat: chat,
|
||||||
|
contact: contact,
|
||||||
|
localAlias: chat.chatInfo.localAlias,
|
||||||
|
onSearch: { focusSearch() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if case let .group(groupInfo) = cInfo {
|
||||||
|
Button {
|
||||||
|
Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
|
||||||
|
} label: {
|
||||||
|
ChatInfoToolbar(chat: chat)
|
||||||
|
.tint(theme.colors.primary)
|
||||||
|
}
|
||||||
|
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
||||||
|
GroupChatInfoView(
|
||||||
|
chat: chat,
|
||||||
|
groupInfo: Binding(
|
||||||
|
get: { groupInfo },
|
||||||
|
set: { gInfo in
|
||||||
|
chat.chatInfo = .group(groupInfo: gInfo)
|
||||||
|
chat.created = Date.now
|
||||||
|
}
|
||||||
|
),
|
||||||
|
onSearch: { focusSearch() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if case .local = cInfo {
|
||||||
ChatInfoToolbar(chat: chat)
|
ChatInfoToolbar(chat: chat)
|
||||||
}
|
}
|
||||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
}.privacySensitive(protectScreen)
|
||||||
ChatInfoView(
|
|
||||||
chat: chat,
|
|
||||||
contact: contact,
|
|
||||||
localAlias: chat.chatInfo.localAlias,
|
|
||||||
onSearch: { focusSearch() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if case let .group(groupInfo) = cInfo {
|
|
||||||
Button {
|
|
||||||
Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
|
|
||||||
} label: {
|
|
||||||
ChatInfoToolbar(chat: chat)
|
|
||||||
.tint(theme.colors.primary)
|
|
||||||
}
|
|
||||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
|
||||||
GroupChatInfoView(
|
|
||||||
chat: chat,
|
|
||||||
groupInfo: Binding(
|
|
||||||
get: { groupInfo },
|
|
||||||
set: { gInfo in
|
|
||||||
chat.chatInfo = .group(groupInfo: gInfo)
|
|
||||||
chat.created = Date.now
|
|
||||||
}
|
|
||||||
),
|
|
||||||
onSearch: { focusSearch() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if case .local = cInfo {
|
|
||||||
ChatInfoToolbar(chat: chat)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
let isLoading = im.isLoading && im.showLoadingProgress
|
Group {
|
||||||
if selectedChatItems != nil {
|
let isLoading = im.isLoading && im.showLoadingProgress
|
||||||
Button {
|
if selectedChatItems != nil {
|
||||||
withAnimation {
|
Button {
|
||||||
selectedChatItems = nil
|
withAnimation {
|
||||||
}
|
selectedChatItems = nil
|
||||||
} label: {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch cInfo {
|
|
||||||
case let .direct(contact):
|
|
||||||
HStack {
|
|
||||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
|
||||||
if callsPrefEnabled {
|
|
||||||
if chatModel.activeCall == nil {
|
|
||||||
callButton(contact, .audio, imageName: "phone")
|
|
||||||
.disabled(!contact.ready || !contact.active)
|
|
||||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
|
||||||
endCallButton(call)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Menu {
|
} label: {
|
||||||
if !isLoading {
|
Text("Cancel")
|
||||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
}
|
||||||
Button {
|
} else {
|
||||||
CallController.shared.startCall(contact, .video)
|
switch cInfo {
|
||||||
} label: {
|
case let .direct(contact):
|
||||||
Label("Video call", systemImage: "video")
|
HStack {
|
||||||
}
|
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||||
.disabled(!contact.ready || !contact.active)
|
if callsPrefEnabled {
|
||||||
|
if chatModel.activeCall == nil {
|
||||||
|
callButton(contact, .audio, imageName: "phone")
|
||||||
|
.disabled(!contact.ready || !contact.active)
|
||||||
|
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||||
|
endCallButton(call)
|
||||||
}
|
}
|
||||||
searchButton()
|
|
||||||
ToggleNtfsButton(chat: chat)
|
|
||||||
.disabled(!contact.ready || !contact.active)
|
|
||||||
}
|
}
|
||||||
} label: {
|
Menu {
|
||||||
Image(systemName: "ellipsis")
|
if !isLoading {
|
||||||
.tint(isLoading ? Color.clear : nil)
|
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||||
.overlay { if isLoading { ProgressView() } }
|
Button {
|
||||||
}
|
CallController.shared.startCall(contact, .video)
|
||||||
}
|
} label: {
|
||||||
case let .group(groupInfo):
|
Label("Video call", systemImage: "video")
|
||||||
HStack {
|
}
|
||||||
if groupInfo.canAddMembers {
|
.disabled(!contact.ready || !contact.active)
|
||||||
if (chat.chatInfo.incognito) {
|
|
||||||
groupLinkButton()
|
|
||||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
|
||||||
GroupLinkView(
|
|
||||||
groupId: groupInfo.groupId,
|
|
||||||
groupLink: $groupLink,
|
|
||||||
groupLinkMemberRole: $groupLinkMemberRole,
|
|
||||||
showTitle: true,
|
|
||||||
creatingGroup: false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
searchButton()
|
||||||
addMembersButton()
|
ToggleNtfsButton(chat: chat)
|
||||||
|
.disabled(!contact.ready || !contact.active)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
.tint(isLoading ? Color.clear : nil)
|
||||||
|
.overlay { if isLoading { ProgressView() } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Menu {
|
case let .group(groupInfo):
|
||||||
if !isLoading {
|
HStack {
|
||||||
searchButton()
|
if groupInfo.canAddMembers {
|
||||||
ToggleNtfsButton(chat: chat)
|
if (chat.chatInfo.incognito) {
|
||||||
|
groupLinkButton()
|
||||||
|
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||||
|
GroupLinkView(
|
||||||
|
groupId: groupInfo.groupId,
|
||||||
|
groupLink: $groupLink,
|
||||||
|
groupLinkMemberRole: $groupLinkMemberRole,
|
||||||
|
showTitle: true,
|
||||||
|
creatingGroup: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addMembersButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Menu {
|
||||||
|
if !isLoading {
|
||||||
|
searchButton()
|
||||||
|
ToggleNtfsButton(chat: chat)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
.tint(isLoading ? Color.clear : nil)
|
||||||
|
.overlay { if isLoading { ProgressView() } }
|
||||||
}
|
}
|
||||||
} label: {
|
|
||||||
Image(systemName: "ellipsis")
|
|
||||||
.tint(isLoading ? Color.clear : nil)
|
|
||||||
.overlay { if isLoading { ProgressView() } }
|
|
||||||
}
|
}
|
||||||
|
case .local:
|
||||||
|
searchButton()
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
case .local:
|
|
||||||
searchButton()
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
}.privacySensitive(protectScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ struct ChatListView: View {
|
|||||||
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
||||||
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
|
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
|
||||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||||
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
@@ -119,11 +120,14 @@ struct ChatListView: View {
|
|||||||
.modifier(
|
.modifier(
|
||||||
Sheet(isPresented: $userPickerShown) {
|
Sheet(isPresented: $userPickerShown) {
|
||||||
UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
|
UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
|
||||||
|
.privacySensitive(protectScreen)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.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) {
|
||||||
@@ -140,6 +144,7 @@ struct ChatListView: View {
|
|||||||
.background(theme.colors.background)
|
.background(theme.colors.background)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarHidden(searchMode || oneHandUI)
|
.navigationBarHidden(searchMode || oneHandUI)
|
||||||
|
.privacySensitive(protectScreen)
|
||||||
}
|
}
|
||||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
.onDisappear() { activeUserPickerSheet = nil }
|
.onDisappear() { activeUserPickerSheet = nil }
|
||||||
@@ -213,6 +218,7 @@ struct ChatListView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
trailingToolbarItem.padding(.bottom, padding)
|
trailingToolbarItem.padding(.bottom, padding)
|
||||||
}
|
}
|
||||||
|
.privacySensitive(protectScreen)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { scrollToSearchBar = true }
|
.onTapGesture { scrollToSearchBar = true }
|
||||||
}
|
}
|
||||||
@@ -241,17 +247,20 @@ struct ChatListView: View {
|
|||||||
unreadBadge(size: 12)
|
unreadBadge(size: 12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.privacySensitive(protectScreen)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
userPickerShown = true
|
userPickerShown = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder var trailingToolbarItem: some View {
|
@ViewBuilder var trailingToolbarItem: some View {
|
||||||
switch chatModel.chatRunning {
|
Group {
|
||||||
case .some(true): NewChatMenuButton()
|
switch chatModel.chatRunning {
|
||||||
case .some(false): chatStoppedIcon()
|
case .some(true): NewChatMenuButton()
|
||||||
case .none: EmptyView()
|
case .some(false): chatStoppedIcon()
|
||||||
}
|
case .none: EmptyView()
|
||||||
|
}
|
||||||
|
}.privacySensitive(protectScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var chatList: some View {
|
@ViewBuilder private var chatList: some View {
|
||||||
@@ -408,6 +417,7 @@ struct SubsStatusIndicator: View {
|
|||||||
@State private var showServersSummary = false
|
@State private var showServersSummary = false
|
||||||
|
|
||||||
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||||
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
@@ -422,6 +432,7 @@ struct SubsStatusIndicator: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(ChatModel.shared.chatRunning != true)
|
.disabled(ChatModel.shared.chatRunning != true)
|
||||||
|
.privacySensitive(protectScreen)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
startTask()
|
startTask()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ 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
|
||||||
|
// Scehe phase is also be inactive while faceID is requested
|
||||||
|
@Published var biometricAuth: Bool = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PrivacySensitive: ViewModifier {
|
private struct PrivacySensitive: ViewModifier {
|
||||||
@@ -22,7 +24,7 @@ private struct PrivacySensitive: ViewModifier {
|
|||||||
if !protectScreen {
|
if !protectScreen {
|
||||||
content
|
content
|
||||||
} else {
|
} else {
|
||||||
content.privacySensitive(!appSheetState.scenePhaseActive).redacted(reason: .privacy)
|
content.privacySensitive(!appSheetState.scenePhaseActive && !appSheetState.biometricAuth).redacted(reason: .privacy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,11 @@ func systemAuthenticate(_ reason: String, _ completed: @escaping (LAResult) -> V
|
|||||||
var authAvailabilityError: NSError?
|
var authAvailabilityError: NSError?
|
||||||
if laContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authAvailabilityError) {
|
if laContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authAvailabilityError) {
|
||||||
logger.debug("DEBUGGING: systemAuthenticate: canEvaluatePolicy callback")
|
logger.debug("DEBUGGING: systemAuthenticate: canEvaluatePolicy callback")
|
||||||
|
AppSheetState.shared.biometricAuth = true
|
||||||
laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
|
laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
|
||||||
logger.debug("DEBUGGING: systemAuthenticate evaluatePolicy callback")
|
logger.debug("DEBUGGING: systemAuthenticate evaluatePolicy callback")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
AppSheetState.shared.biometricAuth = false
|
||||||
if success {
|
if success {
|
||||||
completed(LAResult.success)
|
completed(LAResult.success)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ struct UserProfilesView: View {
|
|||||||
userProfilesView()
|
userProfilesView()
|
||||||
} else {
|
} else {
|
||||||
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
||||||
.onAppear(perform: runAuth)
|
.onAppear {
|
||||||
|
// `scenePhase` check Prevents a feedback loop
|
||||||
|
if AppSheetState.shared.scenePhaseActive { runAuth() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user