mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa9d9ad2a1 | |||
| 62cc26c873 | |||
| 02dc8a1597 | |||
| 95c4e77075 | |||
| 9b6e0ad7fd | |||
| 32946d4628 | |||
| 0da98fbf60 | |||
| 68606ee9ea | |||
| 8ed075f6db | |||
| 2a18770b96 | |||
| dfdb3ca4a2 | |||
| bd85b1063e | |||
| 55c1200007 | |||
| 7abcb6a449 | |||
| a7cc8348fd | |||
| d1b44719f2 | |||
| 995863d78b | |||
| 8f8601eaa4 |
@@ -230,7 +230,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private func mainView() -> some View {
|
private func mainView() -> some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
|
HomeView(showSettings: $showSettings).privacySensitive(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
|
||||||
|
|||||||
@@ -744,21 +744,37 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co
|
|||||||
return (nil, alert)
|
return (nil, alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws {
|
func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws {
|
||||||
let chatId = type.rawValue + id.description
|
let chatId = type.rawValue + id.description
|
||||||
DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) }
|
DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) }
|
||||||
defer { DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } }
|
defer { DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } }
|
||||||
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false)
|
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false)
|
||||||
if case .direct = type, case .contactDeleted = r { return }
|
if case .direct = type, case .contactDeleted = r { return }
|
||||||
if case .contactConnection = type, case .contactConnectionDeleted = r { return }
|
if case .contactConnection = type, case .contactConnectionDeleted = r { return }
|
||||||
if case .group = type, case .groupDeletedUser = r { return }
|
if case .group = type, case .groupDeletedUser = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteChat(_ chat: Chat, notify: Bool? = nil) async {
|
func apiDeleteContact(id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws -> Contact {
|
||||||
|
let type: ChatType = .direct
|
||||||
|
let chatId = type.rawValue + id.description
|
||||||
|
if case .full = chatDeleteMode {
|
||||||
|
DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) }
|
||||||
|
}
|
||||||
|
defer {
|
||||||
|
if case .full = chatDeleteMode {
|
||||||
|
DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false)
|
||||||
|
if case let .contactDeleted(_, contact) = r { return contact }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async {
|
||||||
do {
|
do {
|
||||||
let cInfo = chat.chatInfo
|
let cInfo = chat.chatInfo
|
||||||
try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, notify: notify)
|
try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, chatDeleteMode: chatDeleteMode)
|
||||||
DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) }
|
DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) }
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("deleteChat apiDeleteChat error: \(responseError(error))")
|
logger.error("deleteChat apiDeleteChat error: \(responseError(error))")
|
||||||
@@ -769,6 +785,39 @@ func deleteChat(_ chat: Chat, notify: Bool? = nil) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteChatContact(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async {
|
||||||
|
do {
|
||||||
|
let cInfo = chat.chatInfo
|
||||||
|
let ct = try await apiDeleteContact(id: cInfo.apiId, chatDeleteMode: chatDeleteMode)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch chatDeleteMode {
|
||||||
|
case .full:
|
||||||
|
ChatModel.shared.removeChat(cInfo.id)
|
||||||
|
case .entity:
|
||||||
|
// removeChat forces list update (for ContactsView)
|
||||||
|
ChatModel.shared.removeChat(cInfo.id)
|
||||||
|
ChatModel.shared.addChat(Chat(
|
||||||
|
chatInfo: .direct(contact: ct),
|
||||||
|
chatItems: chat.chatItems
|
||||||
|
))
|
||||||
|
case .messages:
|
||||||
|
// removeChat forces list update (for ChatListView)
|
||||||
|
ChatModel.shared.removeChat(cInfo.id)
|
||||||
|
ChatModel.shared.addChat(Chat(
|
||||||
|
chatInfo: .direct(contact: ct),
|
||||||
|
chatItems: []
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("deleteChatContact apiDeleteContact error: \(responseError(error))")
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title: "Error deleting chat!",
|
||||||
|
message: "Error: \(responseError(error))"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo {
|
func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo {
|
||||||
let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false)
|
let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false)
|
||||||
if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo }
|
if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo }
|
||||||
@@ -1655,6 +1704,11 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
if active(user) {
|
if active(user) {
|
||||||
|
if case let .direct(contact) = cInfo, contact.chatDeleted {
|
||||||
|
var updatedContact = contact
|
||||||
|
updatedContact.chatDeleted = false
|
||||||
|
m.updateContact(updatedContact)
|
||||||
|
}
|
||||||
m.addChatItem(cInfo, cItem)
|
m.addChatItem(cInfo, cItem)
|
||||||
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
|
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
|
||||||
m.increaseUnreadCounter(user: user)
|
m.increaseUnreadCounter(user: user)
|
||||||
|
|||||||
@@ -88,21 +88,36 @@ enum SendReceipts: Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ContactDeleteMode {
|
||||||
|
case full
|
||||||
|
case entity
|
||||||
|
|
||||||
|
public func toChatDeleteMode(notify: Bool) -> ChatDeleteMode {
|
||||||
|
switch self {
|
||||||
|
case .full: .full(notify: notify)
|
||||||
|
case .entity: .entity(notify: notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatInfoView: View {
|
struct ChatInfoView: View {
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
var openedFromChatView: Bool
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
@State var contact: Contact
|
@State var contact: Contact
|
||||||
@Binding var connectionStats: ConnectionStats?
|
|
||||||
@Binding var customUserProfile: Profile?
|
|
||||||
@State var localAlias: String
|
@State var localAlias: String
|
||||||
@Binding var connectionCode: String?
|
@State private var connectionStats: ConnectionStats? = nil
|
||||||
|
@State private var customUserProfile: Profile? = nil
|
||||||
|
@State private var connectionCode: String? = nil
|
||||||
@FocusState private var aliasTextFieldFocused: Bool
|
@FocusState private var aliasTextFieldFocused: Bool
|
||||||
@State private var alert: ChatInfoViewAlert? = nil
|
@State private var alert: ChatInfoViewAlert? = nil
|
||||||
@State private var showDeleteContactActionSheet = false
|
@State private var actionSheet: ChatInfoViewActionSheet? = nil
|
||||||
|
@State private var showConnectContactViaAddressDialog = false
|
||||||
@State private var sendReceipts = SendReceipts.userDefault(true)
|
@State private var sendReceipts = SendReceipts.userDefault(true)
|
||||||
@State private var sendReceiptsUserDefault = true
|
@State private var sendReceiptsUserDefault = true
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
|
@AppStorage(DEFAULT_SHOW_DELETE_CONTACT_NOTICE) private var showDeleteContactNotice = true
|
||||||
|
|
||||||
enum ChatInfoViewAlert: Identifiable {
|
enum ChatInfoViewAlert: Identifiable {
|
||||||
case clearChatAlert
|
case clearChatAlert
|
||||||
@@ -110,6 +125,7 @@ struct ChatInfoView: View {
|
|||||||
case switchAddressAlert
|
case switchAddressAlert
|
||||||
case abortSwitchAddressAlert
|
case abortSwitchAddressAlert
|
||||||
case syncConnectionForceAlert
|
case syncConnectionForceAlert
|
||||||
|
case deleteContactNotice
|
||||||
case queueInfo(info: String)
|
case queueInfo(info: String)
|
||||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||||
|
|
||||||
@@ -120,12 +136,25 @@ struct ChatInfoView: View {
|
|||||||
case .switchAddressAlert: return "switchAddressAlert"
|
case .switchAddressAlert: return "switchAddressAlert"
|
||||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||||
|
case .deleteContactNotice: return "deleteContactNotice"
|
||||||
case let .queueInfo(info): return "queueInfo \(info)"
|
case let .queueInfo(info): return "queueInfo \(info)"
|
||||||
case let .error(title, _): return "error \(title)"
|
case let .error(title, _): return "error \(title)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ChatInfoViewActionSheet: Identifiable {
|
||||||
|
case deleteContactActionSheet
|
||||||
|
case confirmDeleteContactActionSheet(contactDeleteMode: ContactDeleteMode)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .deleteContactActionSheet: return "deleteContactActionSheet"
|
||||||
|
case .confirmDeleteContactActionSheet: return "confirmDeleteContactActionSheet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List {
|
List {
|
||||||
@@ -142,6 +171,23 @@ struct ChatInfoView: View {
|
|||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
||||||
|
connectButton()
|
||||||
|
} else if !contact.active && !contact.chatDeleted {
|
||||||
|
openButton()
|
||||||
|
} else {
|
||||||
|
messageButton()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
callButton()
|
||||||
|
Spacer()
|
||||||
|
videoButton()
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
|
||||||
if let customUserProfile = customUserProfile {
|
if let customUserProfile = customUserProfile {
|
||||||
Section("Incognito") {
|
Section("Incognito") {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -161,9 +207,9 @@ struct ChatInfoView: View {
|
|||||||
connStats.ratchetSyncAllowed {
|
connStats.ratchetSyncAllowed {
|
||||||
synchronizeConnectionButton()
|
synchronizeConnectionButton()
|
||||||
}
|
}
|
||||||
// } else if developerTools {
|
// } else if developerTools {
|
||||||
// synchronizeConnectionButtonForce()
|
// synchronizeConnectionButtonForce()
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
.disabled(!contact.ready || !contact.active)
|
.disabled(!contact.ready || !contact.active)
|
||||||
|
|
||||||
@@ -249,6 +295,23 @@ struct ChatInfoView: View {
|
|||||||
sendReceiptsUserDefault = currentUser.sendRcptsContacts
|
sendReceiptsUserDefault = currentUser.sendRcptsContacts
|
||||||
}
|
}
|
||||||
sendReceipts = SendReceipts.fromBool(contact.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
|
sendReceipts = SendReceipts.fromBool(contact.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let (stats, profile) = try await apiContactInfo(chat.chatInfo.apiId)
|
||||||
|
let (ct, code) = try await apiGetContactCode(chat.chatInfo.apiId)
|
||||||
|
await MainActor.run {
|
||||||
|
connectionStats = stats
|
||||||
|
customUserProfile = profile
|
||||||
|
connectionCode = code
|
||||||
|
if contact.activeConn?.connectionCode != ct.activeConn?.connectionCode {
|
||||||
|
chat.chatInfo = .direct(contact: ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("apiContactInfo or apiGetContactCode error: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.alert(item: $alert) { alertItem in
|
.alert(item: $alert) { alertItem in
|
||||||
switch(alertItem) {
|
switch(alertItem) {
|
||||||
@@ -257,28 +320,47 @@ struct ChatInfoView: View {
|
|||||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
||||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
||||||
|
case .deleteContactNotice: return deleteContactNotice(contact)
|
||||||
case let .queueInfo(info): return queueInfoAlert(info)
|
case let .queueInfo(info): return queueInfoAlert(info)
|
||||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.actionSheet(isPresented: $showDeleteContactActionSheet) {
|
.actionSheet(item: $actionSheet) { sheet in
|
||||||
if contact.ready && contact.active {
|
switch(sheet) {
|
||||||
return ActionSheet(
|
case .deleteContactActionSheet:
|
||||||
title: Text("Delete contact?\nThis cannot be undone!"),
|
var sheetButtons: [ActionSheet.Button] = []
|
||||||
buttons: [
|
sheetButtons.append(
|
||||||
.destructive(Text("Delete and notify contact")) { deleteContact(notify: true) },
|
.destructive(Text("Delete contact")) { actionSheet = .confirmDeleteContactActionSheet(contactDeleteMode: .full) }
|
||||||
.destructive(Text("Delete")) { deleteContact(notify: false) },
|
|
||||||
.cancel()
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
} else {
|
if !contact.chatDeleted {
|
||||||
|
sheetButtons.append(
|
||||||
|
.destructive(Text("Delete contact, keep conversation")) { actionSheet = .confirmDeleteContactActionSheet(contactDeleteMode: .entity) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sheetButtons.append(.cancel())
|
||||||
return ActionSheet(
|
return ActionSheet(
|
||||||
title: Text("Delete contact?\nThis cannot be undone!"),
|
title: Text("Delete contact?"),
|
||||||
buttons: [
|
buttons: sheetButtons
|
||||||
.destructive(Text("Delete")) { deleteContact() },
|
|
||||||
.cancel()
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
case let .confirmDeleteContactActionSheet(contactDeleteMode):
|
||||||
|
if contact.ready && contact.active {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Notify contact?\nThis cannot be undone!"),
|
||||||
|
buttons: [
|
||||||
|
.destructive(Text("Delete and notify contact")) { deleteContact(chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: true)) },
|
||||||
|
.destructive(Text("Delete without notification")) { deleteContact(chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false)) },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Confirm contact deletion.\nThis cannot be undone!"),
|
||||||
|
buttons: [
|
||||||
|
.destructive(Text("Delete")) { deleteContact(chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false)) },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,6 +431,83 @@ struct ChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when contact is a "contact card"
|
||||||
|
private func connectButton() -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "message.fill", title: "connect")
|
||||||
|
.onTapGesture {
|
||||||
|
showConnectContactViaAddressDialog = true
|
||||||
|
}
|
||||||
|
.confirmationDialog("Connect with \(contact.chatViewName)", isPresented: $showConnectContactViaAddressDialog, titleVisibility: .visible) {
|
||||||
|
Button("Use current profile") { connectContactViaAddress_(contact, false) }
|
||||||
|
Button("Use new incognito profile") { connectContactViaAddress_(contact, true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func connectContactViaAddress_(_ contact: Contact, _ incognito: Bool) {
|
||||||
|
Task {
|
||||||
|
let ok = await connectContactViaAddress(contact.contactId, incognito)
|
||||||
|
if ok {
|
||||||
|
await MainActor.run {
|
||||||
|
if openedFromChatView {
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
if contact.chatDeleted {
|
||||||
|
var updatedContact = contact
|
||||||
|
updatedContact.chatDeleted = false
|
||||||
|
chatModel.updateContact(updatedContact)
|
||||||
|
}
|
||||||
|
chatModel.chatId = chat.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openButton() -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "message.fill", title: "open")
|
||||||
|
.onTapGesture {
|
||||||
|
if openedFromChatView {
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
chatModel.chatId = chat.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO show keyboard
|
||||||
|
private func messageButton() -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "message.fill", title: "message")
|
||||||
|
.onTapGesture {
|
||||||
|
if openedFromChatView {
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
if contact.chatDeleted {
|
||||||
|
var updatedContact = contact
|
||||||
|
updatedContact.chatDeleted = false
|
||||||
|
chatModel.updateContact(updatedContact)
|
||||||
|
}
|
||||||
|
chatModel.chatId = chat.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(!contact.sendMsgEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func callButton() -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "phone.fill", title: "call")
|
||||||
|
.onTapGesture {
|
||||||
|
CallController.shared.startCall(contact, .audio)
|
||||||
|
}
|
||||||
|
.disabled(!contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func videoButton() -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "video.fill", title: "video")
|
||||||
|
.onTapGesture {
|
||||||
|
CallController.shared.startCall(contact, .video)
|
||||||
|
}
|
||||||
|
.disabled(!contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall != nil)
|
||||||
|
}
|
||||||
|
|
||||||
private func verifyCodeButton(_ code: String) -> some View {
|
private func verifyCodeButton(_ code: String) -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
VerifyCodeView(
|
VerifyCodeView(
|
||||||
@@ -452,9 +611,9 @@ struct ChatInfoView: View {
|
|||||||
|
|
||||||
private func deleteContactButton() -> some View {
|
private func deleteContactButton() -> some View {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
showDeleteContactActionSheet = true
|
actionSheet = .deleteContactActionSheet
|
||||||
} label: {
|
} label: {
|
||||||
Label("Delete contact", systemImage: "trash")
|
Label("Delete contact", systemImage: "person.badge.minus")
|
||||||
.foregroundColor(Color.red)
|
.foregroundColor(Color.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,17 +627,34 @@ struct ChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteContact(notify: Bool? = nil) {
|
private func deleteContact(chatDeleteMode: ChatDeleteMode) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, notify: notify)
|
let ct = try await apiDeleteContact(id: chat.chatInfo.apiId, chatDeleteMode: chatDeleteMode)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
dismiss()
|
dismiss()
|
||||||
chatModel.chatId = nil
|
chatModel.chatId = nil
|
||||||
chatModel.removeChat(chat.chatInfo.id)
|
switch chatDeleteMode {
|
||||||
|
case .full:
|
||||||
|
chatModel.removeChat(chat.chatInfo.id)
|
||||||
|
case .entity:
|
||||||
|
chatModel.updateContact(ct)
|
||||||
|
// dismissing sheet when opened from ChatView closes deleteContactNotice alert,
|
||||||
|
// also it makes less sense to show this alert as user is already in Chats tab
|
||||||
|
if showDeleteContactNotice && !openedFromChatView {
|
||||||
|
alert = .deleteContactNotice
|
||||||
|
}
|
||||||
|
case .messages:
|
||||||
|
logger.warning("ChatInfoView deleteContact case .messages should be unreachable")
|
||||||
|
chatModel.removeChat(chat.chatInfo.id)
|
||||||
|
chatModel.addChat(Chat(
|
||||||
|
chatInfo: .direct(contact: ct),
|
||||||
|
chatItems: []
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
|
logger.error("ChatInfoView deleteContact apiDeleteContact error: \(responseError(error))")
|
||||||
let a = getErrorAlert(error, "Error deleting contact")
|
let a = getErrorAlert(error, "Error deleting contact")
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
alert = .error(title: a.title, error: a.message)
|
alert = .error(title: a.title, error: a.message)
|
||||||
@@ -563,6 +739,38 @@ struct ChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func deleteContactNotice(_ contact: Contact) -> Alert {
|
||||||
|
return Alert(
|
||||||
|
title: Text("Contact deleted!"),
|
||||||
|
message: Text("You can still view conversation with \(contact.displayName) in the Chats tab."),
|
||||||
|
primaryButton: .default(Text("Don't show again")) {
|
||||||
|
showDeleteContactNotice = false
|
||||||
|
},
|
||||||
|
secondaryButton: .default(Text("Ok"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InfoViewActionButtonLayout: View {
|
||||||
|
var image: String
|
||||||
|
var title: LocalizedStringKey
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: image)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
Text(title)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.background(Color(.secondarySystemGroupedBackground))
|
||||||
|
.cornerRadius(12.0)
|
||||||
|
.frame(width: 90, height: 60)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
||||||
@@ -611,12 +819,10 @@ func queueInfoAlert(_ info: String) -> Alert {
|
|||||||
struct ChatInfoView_Previews: PreviewProvider {
|
struct ChatInfoView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ChatInfoView(
|
ChatInfoView(
|
||||||
|
openedFromChatView: true,
|
||||||
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
|
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
|
||||||
contact: Contact.sampleData,
|
contact: Contact.sampleData,
|
||||||
connectionStats: Binding.constant(nil),
|
localAlias: ""
|
||||||
customUserProfile: Binding.constant(nil),
|
|
||||||
localAlias: "",
|
|
||||||
connectionCode: Binding.constant(nil)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ struct ChatView: View {
|
|||||||
@State private var showAddMembersSheet: Bool = false
|
@State private var showAddMembersSheet: Bool = false
|
||||||
@State private var composeState = ComposeState()
|
@State private var composeState = ComposeState()
|
||||||
@State private var keyboardVisible = false
|
@State private var keyboardVisible = false
|
||||||
@State private var connectionStats: ConnectionStats?
|
|
||||||
@State private var customUserProfile: Profile?
|
|
||||||
@State private var connectionCode: String?
|
|
||||||
@State private var tableView: UITableView?
|
@State private var tableView: UITableView?
|
||||||
@State private var loadingItems = false
|
@State private var loadingItems = false
|
||||||
@State private var firstPage = false
|
@State private var firstPage = false
|
||||||
@@ -45,6 +42,7 @@ struct ChatView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
viewBody
|
viewBody
|
||||||
|
.toolbarBackground(.visible, for: .navigationBar)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
.keyboardPadding()
|
.keyboardPadding()
|
||||||
} else {
|
} else {
|
||||||
@@ -111,32 +109,17 @@ struct ChatView: View {
|
|||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
if case let .direct(contact) = cInfo {
|
if case let .direct(contact) = cInfo {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
showChatInfoSheet = true
|
||||||
do {
|
|
||||||
let (stats, profile) = try await apiContactInfo(chat.chatInfo.apiId)
|
|
||||||
let (ct, code) = try await apiGetContactCode(chat.chatInfo.apiId)
|
|
||||||
await MainActor.run {
|
|
||||||
connectionStats = stats
|
|
||||||
customUserProfile = profile
|
|
||||||
connectionCode = code
|
|
||||||
if contact.activeConn?.connectionCode != ct.activeConn?.connectionCode {
|
|
||||||
chat.chatInfo = .direct(contact: ct)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
logger.error("apiContactInfo or apiGetContactCode error: \(responseError(error))")
|
|
||||||
}
|
|
||||||
await MainActor.run { showChatInfoSheet = true }
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
ChatInfoToolbar(chat: chat)
|
ChatInfoToolbar(chat: chat)
|
||||||
}
|
}
|
||||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: {
|
.appSheet(isPresented: $showChatInfoSheet) {
|
||||||
connectionStats = nil
|
ChatInfoView(
|
||||||
customUserProfile = nil
|
openedFromChatView: true,
|
||||||
connectionCode = nil
|
chat: chat,
|
||||||
}) {
|
contact: contact,
|
||||||
ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: $customUserProfile, localAlias: chat.chatInfo.localAlias, connectionCode: $connectionCode)
|
localAlias: chat.chatInfo.localAlias
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if case let .group(groupInfo) = cInfo {
|
} else if case let .group(groupInfo) = cInfo {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -64,10 +64,11 @@ struct GroupMemberInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func knownDirectChat(_ contactId: Int64) -> Chat? {
|
private func knownDirectChat(_ contactId: Int64) -> (Chat, Contact)? {
|
||||||
if let chat = chatModel.getContactChat(contactId),
|
if let chat = chatModel.getContactChat(contactId),
|
||||||
chat.chatInfo.contact?.directOrUsed == true {
|
let contact = chat.chatInfo.contact,
|
||||||
return chat
|
contact.directOrUsed == true {
|
||||||
|
return (chat, contact)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -80,18 +81,35 @@ struct GroupMemberInfoView: View {
|
|||||||
List {
|
List {
|
||||||
groupMemberInfoHeader(member)
|
groupMemberInfoHeader(member)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
if let contactId = member.memberContactId, let (chat, contact) = knownDirectChat(contactId) {
|
||||||
|
knownDirectChatButton(chat)
|
||||||
|
Spacer()
|
||||||
|
callButton(contact)
|
||||||
|
Spacer()
|
||||||
|
videoButton(contact)
|
||||||
|
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||||
|
if let contactId = member.memberContactId {
|
||||||
|
newDirectChatButton(contactId)
|
||||||
|
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
|
||||||
|
createMemberContactButton()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
InfoViewActionButtonLayout(image: "phone.fill", title: "call")
|
||||||
|
.disabled(true)
|
||||||
|
Spacer()
|
||||||
|
InfoViewActionButtonLayout(image: "video.fill", title: "video")
|
||||||
|
.disabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
|
||||||
if member.memberActive {
|
if member.memberActive {
|
||||||
Section {
|
Section {
|
||||||
if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) {
|
|
||||||
knownDirectChatButton(chat)
|
|
||||||
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
|
||||||
if let contactId = member.memberContactId {
|
|
||||||
newDirectChatButton(contactId)
|
|
||||||
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
|
|
||||||
createMemberContactButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let code = connectionCode { verifyCodeButton(code) }
|
if let code = connectionCode { verifyCodeButton(code) }
|
||||||
if let connStats = connectionStats,
|
if let connStats = connectionStats,
|
||||||
connStats.ratchetSyncAllowed {
|
connStats.ratchetSyncAllowed {
|
||||||
@@ -264,58 +282,71 @@ struct GroupMemberInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func knownDirectChatButton(_ chat: Chat) -> some View {
|
func knownDirectChatButton(_ chat: Chat) -> some View {
|
||||||
Button {
|
InfoViewActionButtonLayout(image: "message.fill", title: "message")
|
||||||
dismissAllSheets(animated: true)
|
.onTapGesture {
|
||||||
DispatchQueue.main.async {
|
|
||||||
chatModel.chatId = chat.id
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label("Send direct message", systemImage: "message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDirectChatButton(_ contactId: Int64) -> some View {
|
|
||||||
Button {
|
|
||||||
do {
|
|
||||||
let chat = try apiGetChat(type: .direct, id: contactId)
|
|
||||||
chatModel.addChat(chat)
|
|
||||||
dismissAllSheets(animated: true)
|
dismissAllSheets(animated: true)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
chatModel.chatId = chat.id
|
chatModel.chatId = chat.id
|
||||||
}
|
}
|
||||||
} catch let error {
|
|
||||||
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
|
|
||||||
}
|
}
|
||||||
} label: {
|
}
|
||||||
Label("Send direct message", systemImage: "message")
|
|
||||||
}
|
func newDirectChatButton(_ contactId: Int64) -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "message.fill", title: "message")
|
||||||
|
.onTapGesture {
|
||||||
|
do {
|
||||||
|
let chat = try apiGetChat(type: .direct, id: contactId)
|
||||||
|
chatModel.addChat(chat)
|
||||||
|
dismissAllSheets(animated: true)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
chatModel.chatId = chat.id
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMemberContactButton() -> some View {
|
func createMemberContactButton() -> some View {
|
||||||
Button {
|
InfoViewActionButtonLayout(image: "message.fill", title: "message")
|
||||||
progressIndicator = true
|
.onTapGesture {
|
||||||
Task {
|
progressIndicator = true
|
||||||
do {
|
Task {
|
||||||
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
|
do {
|
||||||
await MainActor.run {
|
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
|
||||||
progressIndicator = false
|
await MainActor.run {
|
||||||
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
|
progressIndicator = false
|
||||||
dismissAllSheets(animated: true)
|
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
|
||||||
chatModel.chatId = memberContact.id
|
dismissAllSheets(animated: true)
|
||||||
chatModel.setContactNetworkStatus(memberContact, .connected)
|
chatModel.chatId = memberContact.id
|
||||||
}
|
chatModel.setContactNetworkStatus(memberContact, .connected)
|
||||||
} catch let error {
|
}
|
||||||
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
|
} catch let error {
|
||||||
let a = getErrorAlert(error, "Error creating member contact")
|
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
|
||||||
await MainActor.run {
|
let a = getErrorAlert(error, "Error creating member contact")
|
||||||
progressIndicator = false
|
await MainActor.run {
|
||||||
alert = .error(title: a.title, error: a.message)
|
progressIndicator = false
|
||||||
|
alert = .error(title: a.title, error: a.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
}
|
||||||
Label("Send direct message", systemImage: "message")
|
|
||||||
}
|
private func callButton(_ contact: Contact) -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "phone.fill", title: "call")
|
||||||
|
.onTapGesture {
|
||||||
|
CallController.shared.startCall(contact, .audio)
|
||||||
|
}
|
||||||
|
.disabled(!contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func videoButton(_ contact: Contact) -> some View {
|
||||||
|
InfoViewActionButtonLayout(image: "video.fill", title: "video")
|
||||||
|
.onTapGesture {
|
||||||
|
CallController.shared.startCall(contact, .video)
|
||||||
|
}
|
||||||
|
.disabled(!contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func groupMemberInfoHeader(_ mem: GroupMember) -> some View {
|
private func groupMemberInfoHeader(_ mem: GroupMember) -> some View {
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ struct ChatHelp: View {
|
|||||||
|
|
||||||
struct ChatHelp_Previews: PreviewProvider {
|
struct ChatHelp_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
@State var showSettings = false
|
return ChatHelp(showSettings: Binding.constant(false))
|
||||||
return ChatHelp(showSettings: $showSettings)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,25 @@ struct ChatListNavLink: View {
|
|||||||
@State private var showJoinGroupDialog = false
|
@State private var showJoinGroupDialog = false
|
||||||
@State private var showContactConnectionInfo = false
|
@State private var showContactConnectionInfo = false
|
||||||
@State private var showInvalidJSON = false
|
@State private var showInvalidJSON = false
|
||||||
@State private var showDeleteContactActionSheet = false
|
@State private var contactNavLinkSheet: ContactNavLinkActionSheet? = nil
|
||||||
@State private var showConnectContactViaAddressDialog = false
|
@State private var showConnectContactViaAddressDialog = false
|
||||||
@State private var inProgress = false
|
@State private var inProgress = false
|
||||||
@State private var progressByTimeout = false
|
@State private var progressByTimeout = false
|
||||||
|
|
||||||
|
@AppStorage(DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE) private var showDeleteConversationNotice = true
|
||||||
|
|
||||||
|
enum ContactNavLinkActionSheet: Identifiable {
|
||||||
|
case deleteContactActionSheet
|
||||||
|
case confirmDeleteContactActionSheet
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .deleteContactActionSheet: return "deleteContactActionSheet"
|
||||||
|
case .confirmDeleteContactActionSheet: return "confirmDeleteContactActionSheet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
switch chat.chatInfo {
|
switch chat.chatInfo {
|
||||||
@@ -67,12 +81,12 @@ struct ChatListNavLink: View {
|
|||||||
|
|
||||||
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
|
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
||||||
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
||||||
.frame(height: rowHeights[dynamicTypeSize])
|
.frame(height: rowHeights[dynamicTypeSize])
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
Button {
|
Button {
|
||||||
showDeleteContactActionSheet = true
|
contactNavLinkSheet = .deleteContactActionSheet
|
||||||
} label: {
|
} label: {
|
||||||
Label("Delete", systemImage: "trash")
|
Label("Delete", systemImage: "trash")
|
||||||
}
|
}
|
||||||
@@ -100,7 +114,7 @@ struct ChatListNavLink: View {
|
|||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
if contact.ready || !contact.active {
|
if contact.ready || !contact.active {
|
||||||
showDeleteContactActionSheet = true
|
contactNavLinkSheet = .deleteContactActionSheet
|
||||||
} else {
|
} else {
|
||||||
AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact))
|
AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact))
|
||||||
}
|
}
|
||||||
@@ -112,24 +126,53 @@ struct ChatListNavLink: View {
|
|||||||
.frame(height: rowHeights[dynamicTypeSize])
|
.frame(height: rowHeights[dynamicTypeSize])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.actionSheet(isPresented: $showDeleteContactActionSheet) {
|
.actionSheet(item: $contactNavLinkSheet) { sheet in
|
||||||
if contact.ready && contact.active {
|
switch(sheet) {
|
||||||
return ActionSheet(
|
case .deleteContactActionSheet:
|
||||||
title: Text("Delete contact?\nThis cannot be undone!"),
|
if contact.contactStatus == .deletedByUser {
|
||||||
buttons: [
|
return ActionSheet(
|
||||||
.destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } },
|
title: Text("Delete conversation?\nThis cannot be undone!"),
|
||||||
.destructive(Text("Delete")) { Task { await deleteChat(chat, notify: false) } },
|
buttons: [
|
||||||
.cancel()
|
.destructive(Text("Delete conversation")) { Task { await deleteChatContact(chat, chatDeleteMode: .full(notify: false)) } },
|
||||||
]
|
.cancel()
|
||||||
)
|
]
|
||||||
} else {
|
)
|
||||||
return ActionSheet(
|
} else {
|
||||||
title: Text("Delete contact?\nThis cannot be undone!"),
|
return ActionSheet(
|
||||||
buttons: [
|
title: Text("Delete contact?"),
|
||||||
.destructive(Text("Delete")) { Task { await deleteChat(chat) } },
|
buttons: [
|
||||||
.cancel()
|
.destructive(Text("Delete contact")) { contactNavLinkSheet = .confirmDeleteContactActionSheet },
|
||||||
]
|
.destructive(Text("Only delete conversation")) {
|
||||||
)
|
Task {
|
||||||
|
await deleteChatContact(chat, chatDeleteMode: .messages)
|
||||||
|
if showDeleteConversationNotice {
|
||||||
|
AlertManager.shared.showAlert(deleteConversationNotice(contact))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case .confirmDeleteContactActionSheet:
|
||||||
|
if contact.ready && contact.active {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Notify contact?\nThis cannot be undone!"),
|
||||||
|
buttons: [
|
||||||
|
.destructive(Text("Delete and notify contact")) { Task { await deleteChatContact(chat, chatDeleteMode: .full(notify: true)) } },
|
||||||
|
.destructive(Text("Delete without notification")) { Task { await deleteChatContact(chat, chatDeleteMode: .full(notify: false)) } },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Confirm contact deletion.\nThis cannot be undone!"),
|
||||||
|
buttons: [
|
||||||
|
.destructive(Text("Delete")) { Task { await deleteChatContact(chat, chatDeleteMode: .full(notify: false)) } },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,6 +482,17 @@ struct ChatListNavLink: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func deleteConversationNotice(_ contact: Contact) -> Alert {
|
||||||
|
return Alert(
|
||||||
|
title: Text("Conversation deleted!"),
|
||||||
|
message: Text("You can still send messages to \(contact.displayName) from the Contacts tab."),
|
||||||
|
primaryButton: .default(Text("Don't show again")) {
|
||||||
|
showDeleteConversationNotice = false
|
||||||
|
},
|
||||||
|
secondaryButton: .default(Text("Ok"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func deletePendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert {
|
private func deletePendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Delete pending connection"),
|
title: Text("Delete pending connection"),
|
||||||
|
|||||||
@@ -9,18 +9,19 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
|
||||||
struct ChatListView: View {
|
struct ChatListView<ToolbarContent: View>: View {
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@Binding var showSettings: Bool
|
|
||||||
@State private var searchMode = false
|
@State private var searchMode = false
|
||||||
@FocusState private var searchFocussed
|
@FocusState private var searchFocussed
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
@State private var searchShowingSimplexLink = false
|
@State private var searchShowingSimplexLink = false
|
||||||
@State private var searchChatFilteredBySimplexLink: String? = nil
|
@State private var searchChatFilteredBySimplexLink: String? = nil
|
||||||
@State private var newChatMenuOption: NewChatMenuOption? = nil
|
@State var topVisibleRowIndex: Int? = nil;
|
||||||
@State private var userPickerVisible = false
|
@State private var searchVisible: Bool = true;
|
||||||
@State private var showConnectDesktop = false
|
|
||||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||||
|
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||||
|
let toolbarContent: ToolbarContent?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
@@ -29,46 +30,16 @@ struct ChatListView: View {
|
|||||||
viewBody
|
viewBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var viewBody: some View {
|
init(@ViewBuilder toolbarContent: () -> ToolbarContent?) {
|
||||||
ZStack(alignment: .topLeading) {
|
self.toolbarContent = toolbarContent()
|
||||||
NavStackCompat(
|
|
||||||
isActive: Binding(
|
|
||||||
get: { chatModel.chatId != nil },
|
|
||||||
set: { _ in }
|
|
||||||
),
|
|
||||||
destination: chatView
|
|
||||||
) {
|
|
||||||
VStack {
|
|
||||||
if chatModel.chats.isEmpty {
|
|
||||||
onboardingButtons()
|
|
||||||
}
|
|
||||||
chatListView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if userPickerVisible {
|
|
||||||
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
|
|
||||||
withAnimation {
|
|
||||||
userPickerVisible.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UserPicker(
|
|
||||||
showSettings: $showSettings,
|
|
||||||
showConnectDesktop: $showConnectDesktop,
|
|
||||||
userPickerVisible: $userPickerVisible
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showConnectDesktop) {
|
|
||||||
ConnectDesktopView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var chatListView: some View {
|
private var viewBody: some View {
|
||||||
VStack {
|
VStack {
|
||||||
chatList
|
chatList
|
||||||
}
|
}
|
||||||
.onDisappear() { withAnimation { userPickerVisible = false } }
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
.refreshable {
|
.refreshable {
|
||||||
AlertManager.shared.showAlert(Alert(
|
AlertManager.shared.showAlert(Alert(
|
||||||
title: Text("Reconnect servers?"),
|
title: Text("Reconnect servers?"),
|
||||||
@@ -86,82 +57,34 @@ struct ChatListView: View {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.navigationBarHidden(searchMode)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
let user = chatModel.currentUser ?? User.sampleData
|
|
||||||
ZStack(alignment: .topTrailing) {
|
|
||||||
ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel))
|
|
||||||
.padding(.trailing, 4)
|
|
||||||
let allRead = chatModel.users
|
|
||||||
.filter { u in !u.user.activeUser && !u.user.hidden }
|
|
||||||
.allSatisfy { u in u.unreadCount == 0 }
|
|
||||||
if !allRead {
|
|
||||||
unreadBadge(size: 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onTapGesture {
|
|
||||||
if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
|
|
||||||
withAnimation {
|
|
||||||
userPickerVisible.toggle()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showSettings = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToolbarItem(placement: .principal) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text("Chats")
|
|
||||||
.font(.headline)
|
|
||||||
if chatModel.chats.count > 0 {
|
|
||||||
toggleFilterButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
}
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
switch chatModel.chatRunning {
|
|
||||||
case .some(true): NewChatMenuButton(newChatMenuOption: $newChatMenuOption)
|
|
||||||
case .some(false): chatStoppedIcon()
|
|
||||||
case .none: EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggleFilterButton() -> some View {
|
|
||||||
Button {
|
|
||||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var chatList: some View {
|
@ViewBuilder private var chatList: some View {
|
||||||
let cs = filteredChats()
|
let cs = filteredChats()
|
||||||
ZStack {
|
ZStack(alignment: .top) {
|
||||||
VStack {
|
VStack {
|
||||||
List {
|
ScrollViewReader { scrollViewProxy in
|
||||||
if !chatModel.chats.isEmpty {
|
List {
|
||||||
ChatListSearchBar(
|
Color.clear
|
||||||
searchMode: $searchMode,
|
.frame(height: oneHandUI ? 80 : 30)
|
||||||
searchFocussed: $searchFocussed,
|
ForEach(cs.indices, id: \.self) { index in
|
||||||
searchText: $searchText,
|
ChatListNavLink(chat: cs[index])
|
||||||
searchShowingSimplexLink: $searchShowingSimplexLink,
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
|
.padding(.trailing, -16)
|
||||||
)
|
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(cs[index].chatInfo.id))
|
||||||
.listRowSeparator(.hidden)
|
.background(GeometryReader { proxy in
|
||||||
.frame(maxWidth: .infinity)
|
Color.clear
|
||||||
|
.onAppear {
|
||||||
|
updateTopVisibleRowIndex(proxy: proxy, index: index)
|
||||||
|
}
|
||||||
|
.onChange(of: proxy.frame(in: .named("SCROLL")).minY) { _ in
|
||||||
|
updateTopVisibleRowIndex(proxy: proxy, index: index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.offset(x: -8)
|
||||||
}
|
}
|
||||||
ForEach(cs, id: \.viewId) { chat in
|
.coordinateSpace(name: "SCROLL")
|
||||||
ChatListNavLink(chat: chat)
|
|
||||||
.padding(.trailing, -16)
|
|
||||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
|
||||||
}
|
|
||||||
.offset(x: -8)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: chatModel.chatId) { _ in
|
.onChange(of: chatModel.chatId) { _ in
|
||||||
@@ -171,7 +94,74 @@ struct ChatListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cs.isEmpty && !chatModel.chats.isEmpty {
|
if cs.isEmpty && !chatModel.chats.isEmpty {
|
||||||
Text("No filtered chats").foregroundColor(.secondary)
|
Text("No filtered chats")
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
if let tbcontent = toolbarContent, oneHandUI, #available(iOS 16.0, *), !searchFocussed {
|
||||||
|
tbcontent
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
.toolbar(.hidden, for: .bottomBar)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !chatModel.chats.isEmpty && searchVisible {
|
||||||
|
ChatListSearchBar(
|
||||||
|
searchMode: $searchMode,
|
||||||
|
searchFocussed: $searchFocussed,
|
||||||
|
searchText: $searchText,
|
||||||
|
searchShowingSimplexLink: $searchShowingSimplexLink,
|
||||||
|
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
|
||||||
|
)
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(10)
|
||||||
|
.transition(.move(edge: .top))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(.bar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private func updateTopVisibleRowIndex(proxy: GeometryProxy, index: Int) {
|
||||||
|
let frame = proxy.frame(in: .named("SCROLL"))
|
||||||
|
|
||||||
|
if (oneHandUI) {
|
||||||
|
let screenHeight = UIScreen.main.bounds.height
|
||||||
|
|
||||||
|
if frame.maxY <= screenHeight && frame.maxY > screenHeight - frame.height / 2 {
|
||||||
|
if topVisibleRowIndex != index {
|
||||||
|
withAnimation {
|
||||||
|
searchVisible = if let topVisibleRowIndex {
|
||||||
|
topVisibleRowIndex > index || index == 0
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topVisibleRowIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if frame.minY >= 0 && frame.minY < frame.height / 2 {
|
||||||
|
if topVisibleRowIndex != index {
|
||||||
|
withAnimation {
|
||||||
|
searchVisible = if let topVisibleRowIndex {
|
||||||
|
topVisibleRowIndex > index
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topVisibleRowIndex = index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,66 +172,24 @@ struct ChatListView: View {
|
|||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onboardingButtons() -> some View {
|
|
||||||
VStack(alignment: .trailing, spacing: 0) {
|
|
||||||
Path { p in
|
|
||||||
p.move(to: CGPoint(x: 8, y: 0))
|
|
||||||
p.addLine(to: CGPoint(x: 16, y: 10))
|
|
||||||
p.addLine(to: CGPoint(x: 0, y: 10))
|
|
||||||
p.addLine(to: CGPoint(x: 8, y: 0))
|
|
||||||
}
|
|
||||||
.fill(Color.accentColor)
|
|
||||||
.frame(width: 20, height: 10)
|
|
||||||
.padding(.trailing, 12)
|
|
||||||
|
|
||||||
connectButton("Tap to start a new chat") {
|
|
||||||
newChatMenuOption = .newContact
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
Text("You have no chats")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
.padding(.trailing, 6)
|
|
||||||
.frame(maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func connectButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View {
|
|
||||||
Button(action: action) {
|
|
||||||
Text(label)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
}
|
|
||||||
.background(Color.accentColor)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder private func chatView() -> some View {
|
|
||||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
|
||||||
ChatView(chat: chat).onAppear {
|
|
||||||
loadChat(chat: chat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func filteredChats() -> [Chat] {
|
private func filteredChats() -> [Chat] {
|
||||||
if let linkChatId = searchChatFilteredBySimplexLink {
|
if let linkChatId = searchChatFilteredBySimplexLink {
|
||||||
return chatModel.chats.filter { $0.id == linkChatId }
|
return chatModel.chats.filter { $0.id == linkChatId }
|
||||||
} else {
|
} else {
|
||||||
let s = searchString()
|
let s = searchString()
|
||||||
return s == "" && !showUnreadAndFavorites
|
return s == "" && !showUnreadAndFavorites
|
||||||
? chatModel.chats
|
? chatModel.chats.filter { chat in !chat.chatInfo.chatDeleted }
|
||||||
: chatModel.chats.filter { chat in
|
: chatModel.chats.filter { chat in
|
||||||
let cInfo = chat.chatInfo
|
let cInfo = chat.chatInfo
|
||||||
switch cInfo {
|
switch cInfo {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
return s == ""
|
return !contact.chatDeleted && (
|
||||||
? filtered(chat)
|
s == ""
|
||||||
: (viewNameContains(cInfo, s) ||
|
? filtered(chat)
|
||||||
contact.profile.displayName.localizedLowercase.contains(s) ||
|
: (viewNameContains(cInfo, s) ||
|
||||||
contact.fullName.localizedLowercase.contains(s))
|
contact.profile.displayName.localizedLowercase.contains(s) ||
|
||||||
|
contact.fullName.localizedLowercase.contains(s))
|
||||||
|
)
|
||||||
case let .group(gInfo):
|
case let .group(gInfo):
|
||||||
return s == ""
|
return s == ""
|
||||||
? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
|
? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
|
||||||
@@ -282,9 +230,9 @@ struct ChatListSearchBar: View {
|
|||||||
@Binding var searchShowingSimplexLink: Bool
|
@Binding var searchShowingSimplexLink: Bool
|
||||||
@Binding var searchChatFilteredBySimplexLink: String?
|
@Binding var searchChatFilteredBySimplexLink: String?
|
||||||
@State private var ignoreSearchTextChange = false
|
@State private var ignoreSearchTextChange = false
|
||||||
@State private var showScanCodeSheet = false
|
|
||||||
@State private var alert: PlanAndConnectAlert?
|
@State private var alert: PlanAndConnectAlert?
|
||||||
@State private var sheet: PlanAndConnectActionSheet?
|
@State private var sheet: PlanAndConnectActionSheet?
|
||||||
|
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
@@ -301,26 +249,6 @@ struct ChatListSearchBar: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
searchText = ""
|
searchText = ""
|
||||||
}
|
}
|
||||||
} else if !searchFocussed {
|
|
||||||
HStack(spacing: 24) {
|
|
||||||
if m.pasteboardHasStrings {
|
|
||||||
Image(systemName: "doc")
|
|
||||||
.onTapGesture {
|
|
||||||
if let str = UIPasteboard.general.string {
|
|
||||||
searchText = str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(systemName: "qrcode")
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.frame(width: 20, height: 20)
|
|
||||||
.onTapGesture {
|
|
||||||
showScanCodeSheet = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.trailing, 2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
||||||
@@ -335,13 +263,10 @@ struct ChatListSearchBar: View {
|
|||||||
searchText = ""
|
searchText = ""
|
||||||
searchFocussed = false
|
searchFocussed = false
|
||||||
}
|
}
|
||||||
|
} else if m.chats.count > 0 {
|
||||||
|
toggleFilterButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showScanCodeSheet) {
|
|
||||||
NewChatView(selection: .connect, showQRCodeScanner: true)
|
|
||||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) // fixes .refreshable in ChatListView affecting nested view
|
|
||||||
}
|
}
|
||||||
.onChange(of: searchFocussed) { sf in
|
.onChange(of: searchFocussed) { sf in
|
||||||
withAnimation { searchMode = sf }
|
withAnimation { searchMode = sf }
|
||||||
@@ -376,6 +301,21 @@ struct ChatListSearchBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func toggleFilterButton() -> some View {
|
||||||
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
.frame(width: 22, height: 22)
|
||||||
|
Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.foregroundColor(showUnreadAndFavorites ? .accentColor : .secondary)
|
||||||
|
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
|
||||||
|
.onTapGesture {
|
||||||
|
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func connect(_ link: String) {
|
private func connect(_ link: String) {
|
||||||
planAndConnect(
|
planAndConnect(
|
||||||
link,
|
link,
|
||||||
@@ -389,17 +329,6 @@ struct ChatListSearchBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatStoppedIcon() -> some View {
|
|
||||||
Button {
|
|
||||||
AlertManager.shared.showAlertMsg(
|
|
||||||
title: "Chat is stopped",
|
|
||||||
message: "You can start chat via app Settings / Database or by restarting the app"
|
|
||||||
)
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChatListView_Previews: PreviewProvider {
|
struct ChatListView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let chatModel = ChatModel()
|
let chatModel = ChatModel()
|
||||||
@@ -419,10 +348,12 @@ struct ChatListView_Previews: PreviewProvider {
|
|||||||
|
|
||||||
]
|
]
|
||||||
return Group {
|
return Group {
|
||||||
ChatListView(showSettings: Binding.constant(false))
|
ChatListView {
|
||||||
.environmentObject(chatModel)
|
EmptyView()
|
||||||
ChatListView(showSettings: Binding.constant(false))
|
}.environmentObject(chatModel)
|
||||||
.environmentObject(ChatModel())
|
ChatListView {
|
||||||
|
EmptyView()
|
||||||
|
}.environmentObject(ChatModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ struct ChatPreviewView: View {
|
|||||||
} else {
|
} else {
|
||||||
switch (chat.chatInfo) {
|
switch (chat.chatInfo) {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
||||||
chatPreviewInfoText("Tap to Connect")
|
chatPreviewInfoText("Tap to Connect")
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
} else if !contact.ready && contact.activeConn != nil {
|
} else if !contact.ready && contact.activeConn != nil {
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
//
|
||||||
|
// ContactListNavLink.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by spaced4ndy on 06.05.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
struct ContactListNavLink: View {
|
||||||
|
@ObservedObject var chat: Chat
|
||||||
|
@State private var contactNavLinkSheet: ContactNavLinkActionSheet? = nil
|
||||||
|
|
||||||
|
@AppStorage(DEFAULT_SHOW_DELETE_CONTACT_NOTICE) private var showDeleteContactNotice = true
|
||||||
|
|
||||||
|
enum ContactNavLinkActionSheet: Identifiable {
|
||||||
|
case deleteContactActionSheet
|
||||||
|
case confirmDeleteContactActionSheet(contactDeleteMode: ContactDeleteMode)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .deleteContactActionSheet: return "deleteContactActionSheet"
|
||||||
|
case .confirmDeleteContactActionSheet: return "confirmDeleteContactActionSheet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch chat.chatInfo {
|
||||||
|
case let .direct(contact):
|
||||||
|
NavigationLink {
|
||||||
|
ChatInfoView(
|
||||||
|
openedFromChatView: false,
|
||||||
|
chat: chat,
|
||||||
|
contact: contact,
|
||||||
|
localAlias: chat.chatInfo.localAlias
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
HStack{
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
ProfileImage(imageStr: contact.image, size: 38)
|
||||||
|
chatPreviewImageOverlayIcon(contact)
|
||||||
|
.padding([.bottom, .trailing], 1)
|
||||||
|
}
|
||||||
|
.padding(.trailing, 2)
|
||||||
|
|
||||||
|
previewTitle(contact)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
if chat.chatInfo.chatSettings?.favorite ?? false {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 18, height: 18)
|
||||||
|
.padding(.trailing, 1)
|
||||||
|
.foregroundColor(.secondary.opacity(0.65))
|
||||||
|
}
|
||||||
|
if contact.contactConnIncognito {
|
||||||
|
Image(systemName: "theatermasks")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 22, height: 22)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||||
|
toggleFavoriteButton()
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
|
Button {
|
||||||
|
contactNavLinkSheet = .deleteContactActionSheet
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "person.badge.minus")
|
||||||
|
}
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
.actionSheet(item: $contactNavLinkSheet) { sheet in
|
||||||
|
switch(sheet) {
|
||||||
|
case .deleteContactActionSheet:
|
||||||
|
var sheetButtons: [ActionSheet.Button] = []
|
||||||
|
sheetButtons.append(
|
||||||
|
.destructive(Text("Delete contact")) { contactNavLinkSheet = .confirmDeleteContactActionSheet(contactDeleteMode: .full) }
|
||||||
|
)
|
||||||
|
if !contact.chatDeleted {
|
||||||
|
sheetButtons.append(
|
||||||
|
.destructive(Text("Delete contact, keep conversation")) { contactNavLinkSheet = .confirmDeleteContactActionSheet(contactDeleteMode: .entity) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sheetButtons.append(.cancel())
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Delete contact?"),
|
||||||
|
buttons: sheetButtons
|
||||||
|
)
|
||||||
|
case let .confirmDeleteContactActionSheet(contactDeleteMode):
|
||||||
|
if contact.ready && contact.active {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Notify contact?\nThis cannot be undone!"),
|
||||||
|
buttons: [
|
||||||
|
.destructive(Text("Delete and notify contact")) {
|
||||||
|
Task {
|
||||||
|
await deleteChatContact(chat, chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: true))
|
||||||
|
if contactDeleteMode == .entity && showDeleteContactNotice {
|
||||||
|
AlertManager.shared.showAlert(deleteContactNotice(contact))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.destructive(Text("Delete without notification")) {
|
||||||
|
Task {
|
||||||
|
await deleteChatContact(chat, chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false))
|
||||||
|
if contactDeleteMode == .entity && showDeleteContactNotice {
|
||||||
|
AlertManager.shared.showAlert(deleteContactNotice(contact))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Confirm contact deletion.\nThis cannot be undone!"),
|
||||||
|
buttons: [
|
||||||
|
.destructive(Text("Delete")) {
|
||||||
|
Task {
|
||||||
|
await deleteChatContact(chat, chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false))
|
||||||
|
if contactDeleteMode == .entity && showDeleteContactNotice {
|
||||||
|
AlertManager.shared.showAlert(deleteContactNotice(contact))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func previewTitle(_ contact: Contact) -> some View {
|
||||||
|
let t = Text(chat.chatInfo.chatViewName)
|
||||||
|
(
|
||||||
|
contact.verified == true
|
||||||
|
? verifiedIcon + t
|
||||||
|
: t
|
||||||
|
)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var verifiedIcon: Text {
|
||||||
|
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.baselineOffset(1)
|
||||||
|
.kerning(-2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func chatPreviewImageOverlayIcon(_ contact: Contact) -> some View {
|
||||||
|
if !contact.active {
|
||||||
|
inactiveIcon()
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func inactiveIcon() -> some View {
|
||||||
|
Image(systemName: "multiply.circle.fill")
|
||||||
|
.foregroundColor(.secondary.opacity(0.65))
|
||||||
|
.background(Circle().foregroundColor(Color(uiColor: .systemBackground)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteContactNotice(_ contact: Contact) -> Alert {
|
||||||
|
return Alert(
|
||||||
|
title: Text("Contact deleted!"),
|
||||||
|
message: Text("You can still view conversation with \(contact.displayName) in the Chats tab."),
|
||||||
|
primaryButton: .default(Text("Don't show again")) {
|
||||||
|
showDeleteContactNotice = false
|
||||||
|
},
|
||||||
|
secondaryButton: .default(Text("Ok"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func toggleFavoriteButton() -> some View {
|
||||||
|
if chat.chatInfo.chatSettings?.favorite == true {
|
||||||
|
Button {
|
||||||
|
toggleChatFavorite(chat, favorite: false)
|
||||||
|
} label: {
|
||||||
|
Label("Unfav.", systemImage: "star.slash")
|
||||||
|
}
|
||||||
|
.tint(.green)
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
toggleChatFavorite(chat, favorite: true)
|
||||||
|
} label: {
|
||||||
|
Label("Favorite", systemImage: "star.fill")
|
||||||
|
}
|
||||||
|
.tint(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ContactListNavLink(chat: Chat.sampleData)
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
//
|
||||||
|
// ContactsView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by spaced4ndy on 06.05.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
struct ContactsView: View {
|
||||||
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
|
|
||||||
|
@State private var searchMode = false
|
||||||
|
@FocusState private var searchFocussed
|
||||||
|
@State private var searchText = ""
|
||||||
|
|
||||||
|
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||||
|
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
viewBody.scrollDismissesKeyboard(.immediately)
|
||||||
|
} else {
|
||||||
|
viewBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var viewBody: some View {
|
||||||
|
VStack {
|
||||||
|
contactList
|
||||||
|
}
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var contactList: some View {
|
||||||
|
let contactChats = contactChats()
|
||||||
|
let filteredContactChats = filteredContactChats(contactChats)
|
||||||
|
ZStack {
|
||||||
|
VStack {
|
||||||
|
List {
|
||||||
|
if !contactChats.isEmpty {
|
||||||
|
ContactsSearchBar(
|
||||||
|
searchMode: $searchMode,
|
||||||
|
searchFocussed: $searchFocussed,
|
||||||
|
searchText: $searchText
|
||||||
|
)
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
ForEach(filteredContactChats, id: \.viewId) { chat in
|
||||||
|
switch chat.chatInfo {
|
||||||
|
case let .direct(contact):
|
||||||
|
ContactListNavLink(chat: chat)
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(contact.id))
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filteredContactChats.isEmpty && !contactChats.isEmpty {
|
||||||
|
Text("No filtered contacts")
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else if contactChats.isEmpty {
|
||||||
|
Text("No contacts")
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contactChats() -> [Chat] {
|
||||||
|
return chatModel.chats.filter { chat in
|
||||||
|
switch chat.chatInfo {
|
||||||
|
case .direct: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filteredContactChats(_ contactChats: [Chat]) -> [Chat] {
|
||||||
|
let s = searchString()
|
||||||
|
return (
|
||||||
|
s == "" && !showUnreadAndFavorites
|
||||||
|
? contactChats.filter { chat in
|
||||||
|
switch chat.chatInfo {
|
||||||
|
case let .direct(contact): return contact.contactStatus != .deletedByUser
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: contactChats.filter { chat in
|
||||||
|
switch chat.chatInfo {
|
||||||
|
case let .direct(contact):
|
||||||
|
return contact.contactStatus != .deletedByUser && (
|
||||||
|
s == ""
|
||||||
|
? (chat.chatInfo.chatSettings?.favorite ?? false)
|
||||||
|
: (viewNameContains(contact, s) ||
|
||||||
|
contact.profile.displayName.localizedLowercase.contains(s) ||
|
||||||
|
contact.fullName.localizedLowercase.contains(s))
|
||||||
|
)
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.sorted{ $0.chatInfo.displayName.lowercased() < $1.chatInfo.displayName.lowercased() }
|
||||||
|
|
||||||
|
func searchString() -> String {
|
||||||
|
searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewNameContains(_ contact: Contact, _ s: String) -> Bool {
|
||||||
|
contact.chatViewName.localizedLowercase.contains(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContactsSearchBar: View {
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@Binding var searchMode: Bool
|
||||||
|
@FocusState.Binding var searchFocussed: Bool
|
||||||
|
@Binding var searchText: String
|
||||||
|
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
TextField("Search", text: $searchText)
|
||||||
|
.focused($searchFocussed)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
if !searchText.isEmpty {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.onTapGesture {
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.background(Color(.tertiarySystemFill))
|
||||||
|
.cornerRadius(10.0)
|
||||||
|
|
||||||
|
if searchFocussed {
|
||||||
|
Text("Cancel")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.onTapGesture {
|
||||||
|
searchText = ""
|
||||||
|
searchFocussed = false
|
||||||
|
}
|
||||||
|
} else if m.chats.count > 0 {
|
||||||
|
toggleFilterButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: searchFocussed) { sf in
|
||||||
|
withAnimation { searchMode = sf }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleFilterButton() -> some View {
|
||||||
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
.frame(width: 22, height: 22)
|
||||||
|
Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.foregroundColor(showUnreadAndFavorites ? .accentColor : .secondary)
|
||||||
|
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
|
||||||
|
.onTapGesture {
|
||||||
|
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContactsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let chatModel = ChatModel()
|
||||||
|
chatModel.chats = [
|
||||||
|
Chat(
|
||||||
|
chatInfo: ChatInfo.sampleData.direct,
|
||||||
|
chatItems: []
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return Group {
|
||||||
|
ContactsView()
|
||||||
|
.environmentObject(chatModel)
|
||||||
|
ContactsView()
|
||||||
|
.environmentObject(ChatModel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
//
|
||||||
|
// HomeView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by spaced4ndy on 01.05.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
enum HomeTab {
|
||||||
|
case contacts
|
||||||
|
case chats
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeView: View {
|
||||||
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
|
@Binding var showSettings: Bool
|
||||||
|
@State private var homeTab: HomeTab = .chats
|
||||||
|
@State private var userPickerVisible = false
|
||||||
|
@State private var showConnectDesktop = false
|
||||||
|
@State private var newChatMenuOption: NewChatMenuOption? = nil
|
||||||
|
@State private var toolbarHeight: CGFloat = 0
|
||||||
|
|
||||||
|
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottomLeading) {
|
||||||
|
NavStackCompat(
|
||||||
|
isActive: Binding(
|
||||||
|
get: { chatModel.chatId != nil },
|
||||||
|
set: { _ in }
|
||||||
|
),
|
||||||
|
destination: chatView,
|
||||||
|
content: homeView
|
||||||
|
)
|
||||||
|
|
||||||
|
if userPickerVisible {
|
||||||
|
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
|
||||||
|
withAnimation {
|
||||||
|
userPickerVisible.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserPicker(
|
||||||
|
showSettings: $showSettings,
|
||||||
|
showConnectDesktop: $showConnectDesktop,
|
||||||
|
userPickerVisible: $userPickerVisible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showConnectDesktop) {
|
||||||
|
ConnectDesktopView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func toolbar() -> some View {
|
||||||
|
settingsButton()
|
||||||
|
Spacer()
|
||||||
|
contactsButton()
|
||||||
|
Spacer()
|
||||||
|
chatsButton()
|
||||||
|
Spacer()
|
||||||
|
newChatButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func homeView() -> some View {
|
||||||
|
let v = VStack {
|
||||||
|
switch homeTab {
|
||||||
|
case .contacts: withToolbar("Contacts", contactsView)
|
||||||
|
case .chats: withToolbar("Chats", chatListView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
|
toolbar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if #unavailable(iOS 16) {
|
||||||
|
v
|
||||||
|
} else if oneHandUI {
|
||||||
|
v.toolbarBackground(.visible, for: .navigationBar, .bottomBar)
|
||||||
|
} else {
|
||||||
|
v.toolbarBackground(.visible, for: .bottomBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withToolbar<V: View>(_ title: LocalizedStringKey, _ content: () -> V) -> some View {
|
||||||
|
content()
|
||||||
|
.navigationBarTitleDisplayMode(oneHandUI ? .inline : .large)
|
||||||
|
.navigationTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func settingsButton() -> some View {
|
||||||
|
let user = chatModel.currentUser ?? User.sampleData
|
||||||
|
let multiUser = chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1
|
||||||
|
Button {
|
||||||
|
if multiUser {
|
||||||
|
withAnimation {
|
||||||
|
userPickerVisible.toggle()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showSettings = true
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
VStack(spacing: 2) {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
ProfileImage(imageStr: user.image, size: 42, color: Color(uiColor: .quaternaryLabel))
|
||||||
|
.padding(.top, 3)
|
||||||
|
.padding(.trailing, 3)
|
||||||
|
let allRead = chatModel.users
|
||||||
|
.filter { u in !u.user.activeUser && !u.user.hidden }
|
||||||
|
.allSatisfy { u in u.unreadCount == 0 }
|
||||||
|
if !allRead {
|
||||||
|
userUnreadBadge(size: 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func userUnreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||||
|
Circle()
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contactsButton() -> some View {
|
||||||
|
Button {
|
||||||
|
homeTab = .contacts
|
||||||
|
} label: {
|
||||||
|
iconLabel(homeTab == .contacts ? "person.crop.circle.fill" : "person.crop.circle", "Contacts")
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chatsButton() -> some View {
|
||||||
|
Button {
|
||||||
|
homeTab = .chats
|
||||||
|
} label: {
|
||||||
|
iconLabel(homeTab == .chats ? "message.fill" : "message", "Chats")
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func newChatButton() -> some View {
|
||||||
|
if case .some(false) = chatModel.chatRunning {
|
||||||
|
chatsStoppedButton()
|
||||||
|
} else {
|
||||||
|
Menu {
|
||||||
|
Button {
|
||||||
|
newChatMenuOption = .newGroup
|
||||||
|
} label: {
|
||||||
|
Text("Create group")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
newChatMenuOption = .scanPaste
|
||||||
|
} label: {
|
||||||
|
Text("Scan / Paste link")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
newChatMenuOption = .newContact
|
||||||
|
} label: {
|
||||||
|
Text("Add contact")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
iconLabel("square.and.pencil", "New chat")
|
||||||
|
}
|
||||||
|
.sheet(item: $newChatMenuOption) { opt in
|
||||||
|
switch opt {
|
||||||
|
case .newContact: NewChatView(selection: .invite)
|
||||||
|
case .scanPaste: NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||||
|
case .newGroup: AddGroupView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatsStoppedButton() -> some View {
|
||||||
|
Button {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title: "Chat is stopped",
|
||||||
|
message: "You can start chat via app Settings / Database or by restarting the app"
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "exclamationmark.octagon.fill")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
Text("Stopped")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func iconLabel(_ image: String, _ title: LocalizedStringKey) -> some View {
|
||||||
|
VStack(spacing: 2) {
|
||||||
|
Image(systemName: image)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
Text(title)
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
.padding(.top, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func contactsView() -> some View {
|
||||||
|
ContactsView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func chatListView() -> some View {
|
||||||
|
// TODO reverse scale effect for swipe actions
|
||||||
|
ChatListView {
|
||||||
|
HStack {
|
||||||
|
toolbar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func chatView() -> some View {
|
||||||
|
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||||
|
ChatView(chat: chat).onAppear {
|
||||||
|
loadChat(chat: chat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
HomeView(showSettings: Binding.constant(false))
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
|
|
||||||
enum NewChatMenuOption: Identifiable {
|
enum NewChatMenuOption: Identifiable {
|
||||||
case newContact
|
case newContact
|
||||||
|
case scanPaste
|
||||||
case newGroup
|
case newGroup
|
||||||
|
|
||||||
var id: Self { self }
|
var id: Self { self }
|
||||||
@@ -25,6 +26,11 @@ struct NewChatMenuButton: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Text("Add contact")
|
Text("Add contact")
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
newChatMenuOption = .scanPaste
|
||||||
|
} label: {
|
||||||
|
Text("Scan / Paste link")
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
newChatMenuOption = .newGroup
|
newChatMenuOption = .newGroup
|
||||||
} label: {
|
} label: {
|
||||||
@@ -39,6 +45,7 @@ struct NewChatMenuButton: View {
|
|||||||
.sheet(item: $newChatMenuOption) { opt in
|
.sheet(item: $newChatMenuOption) { opt in
|
||||||
switch opt {
|
switch opt {
|
||||||
case .newContact: NewChatView(selection: .invite)
|
case .newContact: NewChatView(selection: .invite)
|
||||||
|
case .scanPaste: NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||||
case .newGroup: AddGroupView()
|
case .newGroup: AddGroupView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ extension AppSettings {
|
|||||||
if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) }
|
if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) }
|
||||||
if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) }
|
if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) }
|
||||||
if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) }
|
if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) }
|
||||||
|
if let val = oneHandUI { def.setValue(val, forKey: DEFAULT_ONE_HAND_UI) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var current: AppSettings {
|
public static var current: AppSettings {
|
||||||
@@ -69,6 +70,7 @@ extension AppSettings {
|
|||||||
c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!)
|
c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!)
|
||||||
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
|
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
|
||||||
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
||||||
|
c.oneHandUI = def.bool(forKey: DEFAULT_ONE_HAND_UI)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ struct AppearanceSettings: View {
|
|||||||
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
|
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
|
||||||
@State private var uiTintColor = getUIAccentColorDefault()
|
@State private var uiTintColor = getUIAccentColorDefault()
|
||||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
||||||
|
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack{
|
VStack{
|
||||||
@@ -39,6 +40,12 @@ struct AppearanceSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Section("Interface") {
|
||||||
|
// settingsRow("hand.wave") {
|
||||||
|
// Toggle("One-hand UI", isOn: $oneHandUI)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
Section("App icon") {
|
Section("App icon") {
|
||||||
HStack {
|
HStack {
|
||||||
updateAppIcon(image: "icon-light", icon: nil, tapped: $iconLightTapped)
|
updateAppIcon(image: "icon-light", icon: nil, tapped: $iconLightTapped)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
|||||||
struct DeveloperView: View {
|
struct DeveloperView: View {
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
||||||
|
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -31,9 +32,6 @@ struct DeveloperView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
settingsRow("terminal") { Text("Chat console") }
|
settingsRow("terminal") { Text("Chat console") }
|
||||||
}
|
}
|
||||||
settingsRow("internaldrive") {
|
|
||||||
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
|
||||||
}
|
|
||||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||||
Toggle("Show developer options", isOn: $developerTools)
|
Toggle("Show developer options", isOn: $developerTools)
|
||||||
}
|
}
|
||||||
@@ -42,6 +40,19 @@ struct DeveloperView: View {
|
|||||||
} footer: {
|
} footer: {
|
||||||
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if developerTools {
|
||||||
|
Section {
|
||||||
|
settingsRow("internaldrive") {
|
||||||
|
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
||||||
|
}
|
||||||
|
settingsRow("hand.wave") {
|
||||||
|
Toggle("One-hand UI", isOn: $oneHandUI)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Developer options")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen"
|
|||||||
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue"
|
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue"
|
||||||
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
|
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
|
||||||
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
||||||
|
let DEFAULT_ONE_HAND_UI = "oneHandUI"
|
||||||
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||||
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
||||||
let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
|
let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
|
||||||
@@ -60,6 +61,8 @@ let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
|
|||||||
let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
|
let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
|
||||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||||
|
let DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||||
|
let DEFAULT_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||||
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||||
|
|
||||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||||
@@ -90,6 +93,7 @@ let appDefaults: [String: Any] = [
|
|||||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||||
DEFAULT_USER_INTERFACE_STYLE: 0,
|
DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||||
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
||||||
|
DEFAULT_ONE_HAND_UI: false,
|
||||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
||||||
@@ -100,6 +104,8 @@ let appDefaults: [String: Any] = [
|
|||||||
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
|
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
|
||||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
||||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||||
|
DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE: true,
|
||||||
|
DEFAULT_SHOW_DELETE_CONTACT_NOTICE: true,
|
||||||
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
|
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
|
||||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
||||||
]
|
]
|
||||||
@@ -448,9 +454,8 @@ struct SettingsView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let chatModel = ChatModel()
|
let chatModel = ChatModel()
|
||||||
chatModel.currentUser = User.sampleData
|
chatModel.currentUser = User.sampleData
|
||||||
@State var showSettings = false
|
|
||||||
|
|
||||||
return SettingsView(showSettings: $showSettings)
|
return SettingsView(showSettings: Binding.constant(false))
|
||||||
.environmentObject(chatModel)
|
.environmentObject(chatModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,6 +403,6 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String {
|
|||||||
|
|
||||||
struct UserProfilesView_Previews: PreviewProvider {
|
struct UserProfilesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
UserProfilesView(showSettings: Binding.constant(true))
|
UserProfilesView(showSettings: Binding.constant(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,7 @@
|
|||||||
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
||||||
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 */; };
|
||||||
|
642CE4952BE2651E00AD7757 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642CE4942BE2651E00AD7757 /* HomeView.swift */; };
|
||||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||||
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 */; };
|
||||||
@@ -167,9 +168,11 @@
|
|||||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
||||||
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
|
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
|
||||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
|
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
|
||||||
|
6485F7C62BE8DB6500FAE413 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6485F7C52BE8DB6500FAE413 /* ContactsView.swift */; };
|
||||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
||||||
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
|
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
|
||||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
||||||
|
64A596452BE90DEC00B69266 /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A596442BE90DEC00B69266 /* ContactListNavLink.swift */; };
|
||||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||||
@@ -445,6 +448,7 @@
|
|||||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
642CE4942BE2651E00AD7757 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.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>"; };
|
||||||
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>"; };
|
||||||
@@ -462,10 +466,12 @@
|
|||||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
||||||
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
|
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
|
||||||
648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = "<group>"; };
|
648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = "<group>"; };
|
||||||
|
6485F7C52BE8DB6500FAE413 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
|
||||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; };
|
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; };
|
||||||
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
|
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
|
||||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||||
|
64A596442BE90DEC00B69266 /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
|
||||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||||
@@ -562,11 +568,13 @@
|
|||||||
5C2E260D27A30E2400F70299 /* Views */ = {
|
5C2E260D27A30E2400F70299 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
642CE4932BE2650500AD7757 /* Home */,
|
||||||
5CB0BA8C282711BC00B3292C /* Onboarding */,
|
5CB0BA8C282711BC00B3292C /* Onboarding */,
|
||||||
3C714775281C080100CB4D4B /* Call */,
|
3C714775281C080100CB4D4B /* Call */,
|
||||||
5C971E1F27AEBF7000C8A3CE /* Helpers */,
|
5C971E1F27AEBF7000C8A3CE /* Helpers */,
|
||||||
5C5F4AC227A5E9AF00B51EF1 /* Chat */,
|
5C5F4AC227A5E9AF00B51EF1 /* Chat */,
|
||||||
5CB9250B27A942F300ACCCDD /* ChatList */,
|
5CB9250B27A942F300ACCCDD /* ChatList */,
|
||||||
|
6485F7C42BE8DB4F00FAE413 /* Contacts */,
|
||||||
5CB924DD27A8622200ACCCDD /* NewChat */,
|
5CB924DD27A8622200ACCCDD /* NewChat */,
|
||||||
5CFA59C22860B04D00863A68 /* Database */,
|
5CFA59C22860B04D00863A68 /* Database */,
|
||||||
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
|
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
|
||||||
@@ -899,6 +907,14 @@
|
|||||||
path = Database;
|
path = Database;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
642CE4932BE2650500AD7757 /* Home */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
642CE4942BE2651E00AD7757 /* HomeView.swift */,
|
||||||
|
);
|
||||||
|
path = Home;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
6440CA01288AEC770062C672 /* Group */ = {
|
6440CA01288AEC770062C672 /* Group */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -913,6 +929,15 @@
|
|||||||
path = Group;
|
path = Group;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
6485F7C42BE8DB4F00FAE413 /* Contacts */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
6485F7C52BE8DB6500FAE413 /* ContactsView.swift */,
|
||||||
|
64A596442BE90DEC00B69266 /* ContactListNavLink.swift */,
|
||||||
|
);
|
||||||
|
path = Contacts;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
8C7D94982B8894D300B7B9E1 /* Migration */ = {
|
8C7D94982B8894D300B7B9E1 /* Migration */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1148,6 +1173,7 @@
|
|||||||
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */,
|
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */,
|
||||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
|
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
|
||||||
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */,
|
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */,
|
||||||
|
64A596452BE90DEC00B69266 /* ContactListNavLink.swift in Sources */,
|
||||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */,
|
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */,
|
||||||
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */,
|
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */,
|
||||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
|
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
|
||||||
@@ -1168,6 +1194,7 @@
|
|||||||
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */,
|
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */,
|
||||||
5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */,
|
5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */,
|
||||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */,
|
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */,
|
||||||
|
6485F7C62BE8DB6500FAE413 /* ContactsView.swift in Sources */,
|
||||||
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */,
|
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */,
|
||||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */,
|
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */,
|
||||||
@@ -1212,6 +1239,7 @@
|
|||||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
|
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
|
||||||
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
||||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
||||||
|
642CE4952BE2651E00AD7757 /* HomeView.swift in Sources */,
|
||||||
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */,
|
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */,
|
||||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public enum ChatCommand {
|
|||||||
case apiConnectPlan(userId: Int64, connReq: String)
|
case apiConnectPlan(userId: Int64, connReq: String)
|
||||||
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
|
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
|
||||||
case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64)
|
case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64)
|
||||||
case apiDeleteChat(type: ChatType, id: Int64, notify: Bool?)
|
case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode)
|
||||||
case apiClearChat(type: ChatType, id: Int64)
|
case apiClearChat(type: ChatType, id: Int64)
|
||||||
case apiListContacts(userId: Int64)
|
case apiListContacts(userId: Int64)
|
||||||
case apiUpdateProfile(userId: Int64, profile: Profile)
|
case apiUpdateProfile(userId: Int64, profile: Profile)
|
||||||
@@ -257,11 +257,7 @@ public enum ChatCommand {
|
|||||||
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
|
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
|
||||||
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
|
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
|
||||||
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
|
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
|
||||||
case let .apiDeleteChat(type, id, notify): if let notify = notify {
|
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)"
|
||||||
return "/_delete \(ref(type, id)) notify=\(onOff(notify))"
|
|
||||||
} else {
|
|
||||||
return "/_delete \(ref(type, id))"
|
|
||||||
}
|
|
||||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||||
case let .apiListContacts(userId): return "/_contacts \(userId)"
|
case let .apiListContacts(userId): return "/_contacts \(userId)"
|
||||||
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
|
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
|
||||||
@@ -485,10 +481,6 @@ public enum ChatCommand {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onOff(_ b: Bool) -> String {
|
|
||||||
b ? "on" : "off"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func onOffParam(_ param: String, _ b: Bool?) -> String {
|
private func onOffParam(_ param: String, _ b: Bool?) -> String {
|
||||||
if let b = b {
|
if let b = b {
|
||||||
return " \(param)=\(onOff(b))"
|
return " \(param)=\(onOff(b))"
|
||||||
@@ -501,6 +493,10 @@ public enum ChatCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func onOff(_ b: Bool) -> String {
|
||||||
|
b ? "on" : "off"
|
||||||
|
}
|
||||||
|
|
||||||
public struct APIResponse: Decodable {
|
public struct APIResponse: Decodable {
|
||||||
var resp: ChatResponse
|
var resp: ChatResponse
|
||||||
}
|
}
|
||||||
@@ -1010,6 +1006,20 @@ public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ChatDeleteMode: Codable {
|
||||||
|
case full(notify: Bool)
|
||||||
|
case entity(notify: Bool)
|
||||||
|
case messages
|
||||||
|
|
||||||
|
var cmdString: String {
|
||||||
|
switch self {
|
||||||
|
case let .full(notify): "full notify=\(onOff(notify))"
|
||||||
|
case let .entity(notify): "entity notify=\(onOff(notify))"
|
||||||
|
case .messages: "messages"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum ConnectionPlan: Decodable {
|
public enum ConnectionPlan: Decodable {
|
||||||
case invitationLink(invitationLinkPlan: InvitationLinkPlan)
|
case invitationLink(invitationLinkPlan: InvitationLinkPlan)
|
||||||
case contactAddress(contactAddressPlan: ContactAddressPlan)
|
case contactAddress(contactAddressPlan: ContactAddressPlan)
|
||||||
@@ -2071,6 +2081,7 @@ public struct AppSettings: Codable, Equatable {
|
|||||||
public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil
|
public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil
|
||||||
public var iosCallKitEnabled: Bool? = nil
|
public var iosCallKitEnabled: Bool? = nil
|
||||||
public var iosCallKitCallsInRecents: Bool? = nil
|
public var iosCallKitCallsInRecents: Bool? = nil
|
||||||
|
public var oneHandUI: Bool? = nil
|
||||||
|
|
||||||
public func prepareForExport() -> AppSettings {
|
public func prepareForExport() -> AppSettings {
|
||||||
var empty = AppSettings()
|
var empty = AppSettings()
|
||||||
@@ -2095,6 +2106,7 @@ public struct AppSettings: Codable, Equatable {
|
|||||||
if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
||||||
if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled }
|
if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||||
if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||||
|
if oneHandUI != def.oneHandUI { empty.oneHandUI = oneHandUI }
|
||||||
return empty
|
return empty
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2119,7 +2131,8 @@ public struct AppSettings: Codable, Equatable {
|
|||||||
confirmDBUpgrades: false,
|
confirmDBUpgrades: false,
|
||||||
androidCallOnLockScreen: AppSettingsLockScreenCalls.show,
|
androidCallOnLockScreen: AppSettingsLockScreenCalls.show,
|
||||||
iosCallKitEnabled: true,
|
iosCallKitEnabled: true,
|
||||||
iosCallKitCallsInRecents: false
|
iosCallKitCallsInRecents: false,
|
||||||
|
oneHandUI: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1287,6 +1287,15 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var chatDeleted: Bool {
|
||||||
|
get {
|
||||||
|
switch self {
|
||||||
|
case let .direct(contact): return contact.chatDeleted
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var sendMsgEnabled: Bool {
|
public var sendMsgEnabled: Bool {
|
||||||
get {
|
get {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -1504,6 +1513,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
var chatTs: Date?
|
var chatTs: Date?
|
||||||
var contactGroupMemberId: Int64?
|
var contactGroupMemberId: Int64?
|
||||||
var contactGrpInvSent: Bool
|
var contactGrpInvSent: Bool
|
||||||
|
public var chatDeleted: Bool
|
||||||
|
|
||||||
public var id: ChatId { get { "@\(contactId)" } }
|
public var id: ChatId { get { "@\(contactId)" } }
|
||||||
public var apiId: Int64 { get { contactId } }
|
public var apiId: Int64 { get { contactId } }
|
||||||
@@ -1570,13 +1580,15 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
mergedPreferences: ContactUserPreferences.sampleData,
|
mergedPreferences: ContactUserPreferences.sampleData,
|
||||||
createdAt: .now,
|
createdAt: .now,
|
||||||
updatedAt: .now,
|
updatedAt: .now,
|
||||||
contactGrpInvSent: false
|
contactGrpInvSent: false,
|
||||||
|
chatDeleted: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ContactStatus: String, Decodable {
|
public enum ContactStatus: String, Decodable {
|
||||||
case active = "active"
|
case active = "active"
|
||||||
case deleted = "deleted"
|
case deleted = "deleted"
|
||||||
|
case deletedByUser = "deletedByUser"
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ContactRef: Decodable, Equatable {
|
public struct ContactRef: Decodable, Equatable {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package chat.simplex.common.views.chatlist
|
package chat.simplex.common.views.home
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
@@ -4,16 +4,12 @@ import androidx.compose.animation.core.Animatable
|
|||||||
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.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.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.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
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
|
||||||
@@ -28,6 +24,7 @@ import chat.simplex.common.views.chat.ChatView
|
|||||||
import chat.simplex.common.views.chatlist.*
|
import chat.simplex.common.views.chatlist.*
|
||||||
import chat.simplex.common.views.database.DatabaseErrorView
|
import chat.simplex.common.views.database.DatabaseErrorView
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.home.*
|
||||||
import chat.simplex.common.views.localauth.VerticalDivider
|
import chat.simplex.common.views.localauth.VerticalDivider
|
||||||
import chat.simplex.common.views.onboarding.*
|
import chat.simplex.common.views.onboarding.*
|
||||||
import chat.simplex.common.views.usersettings.*
|
import chat.simplex.common.views.usersettings.*
|
||||||
@@ -287,7 +284,7 @@ fun StartPartOfScreen(settingsState: SettingsViewState) {
|
|||||||
} 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, settingsState, AppLock::setPerformLA, stopped)
|
HomeView(chatModel, settingsState, AppLock::setPerformLA, stopped)
|
||||||
else
|
else
|
||||||
ShareListView(chatModel, settingsState, stopped)
|
ShareListView(chatModel, settingsState, stopped)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -779,6 +779,7 @@ interface SomeChat {
|
|||||||
val id: ChatId
|
val id: ChatId
|
||||||
val apiId: Long
|
val apiId: Long
|
||||||
val ready: Boolean
|
val ready: Boolean
|
||||||
|
val chatDeleted: Boolean
|
||||||
val sendMsgEnabled: Boolean
|
val sendMsgEnabled: Boolean
|
||||||
val ntfsEnabled: Boolean
|
val ntfsEnabled: Boolean
|
||||||
val incognito: Boolean
|
val incognito: Boolean
|
||||||
@@ -859,6 +860,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = contact.id
|
override val id get() = contact.id
|
||||||
override val apiId get() = contact.apiId
|
override val apiId get() = contact.apiId
|
||||||
override val ready get() = contact.ready
|
override val ready get() = contact.ready
|
||||||
|
override val chatDeleted get() = contact.chatDeleted
|
||||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = contact.ntfsEnabled
|
override val ntfsEnabled get() = contact.ntfsEnabled
|
||||||
override val incognito get() = contact.incognito
|
override val incognito get() = contact.incognito
|
||||||
@@ -883,6 +885,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = groupInfo.id
|
override val id get() = groupInfo.id
|
||||||
override val apiId get() = groupInfo.apiId
|
override val apiId get() = groupInfo.apiId
|
||||||
override val ready get() = groupInfo.ready
|
override val ready get() = groupInfo.ready
|
||||||
|
override val chatDeleted get() = groupInfo.chatDeleted
|
||||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
||||||
override val incognito get() = groupInfo.incognito
|
override val incognito get() = groupInfo.incognito
|
||||||
@@ -907,6 +910,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = noteFolder.id
|
override val id get() = noteFolder.id
|
||||||
override val apiId get() = noteFolder.apiId
|
override val apiId get() = noteFolder.apiId
|
||||||
override val ready get() = noteFolder.ready
|
override val ready get() = noteFolder.ready
|
||||||
|
override val chatDeleted get() = noteFolder.chatDeleted
|
||||||
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
||||||
override val incognito get() = noteFolder.incognito
|
override val incognito get() = noteFolder.incognito
|
||||||
@@ -931,6 +935,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = contactRequest.id
|
override val id get() = contactRequest.id
|
||||||
override val apiId get() = contactRequest.apiId
|
override val apiId get() = contactRequest.apiId
|
||||||
override val ready get() = contactRequest.ready
|
override val ready get() = contactRequest.ready
|
||||||
|
override val chatDeleted get() = contactRequest.chatDeleted
|
||||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
||||||
override val incognito get() = contactRequest.incognito
|
override val incognito get() = contactRequest.incognito
|
||||||
@@ -955,6 +960,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = contactConnection.id
|
override val id get() = contactConnection.id
|
||||||
override val apiId get() = contactConnection.apiId
|
override val apiId get() = contactConnection.apiId
|
||||||
override val ready get() = contactConnection.ready
|
override val ready get() = contactConnection.ready
|
||||||
|
override val chatDeleted get() = contactConnection.chatDeleted
|
||||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
override val incognito get() = contactConnection.incognito
|
override val incognito get() = contactConnection.incognito
|
||||||
@@ -980,6 +986,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = ""
|
override val id get() = ""
|
||||||
override val apiId get() = 0L
|
override val apiId get() = 0L
|
||||||
override val ready get() = false
|
override val ready get() = false
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val sendMsgEnabled get() = false
|
override val sendMsgEnabled get() = false
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
override val incognito get() = false
|
override val incognito get() = false
|
||||||
@@ -1056,6 +1063,7 @@ data class Contact(
|
|||||||
val chatTs: Instant?,
|
val chatTs: Instant?,
|
||||||
val contactGroupMemberId: Long? = null,
|
val contactGroupMemberId: Long? = null,
|
||||||
val contactGrpInvSent: Boolean,
|
val contactGrpInvSent: Boolean,
|
||||||
|
override val chatDeleted: Boolean,
|
||||||
val uiThemes: ThemeModeOverrides? = null,
|
val uiThemes: ThemeModeOverrides? = null,
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Direct
|
override val chatType get() = ChatType.Direct
|
||||||
@@ -1129,6 +1137,7 @@ data class Contact(
|
|||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now(),
|
||||||
chatTs = Clock.System.now(),
|
chatTs = Clock.System.now(),
|
||||||
contactGrpInvSent = false,
|
contactGrpInvSent = false,
|
||||||
|
chatDeleted = false,
|
||||||
uiThemes = null,
|
uiThemes = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1137,7 +1146,8 @@ data class Contact(
|
|||||||
@Serializable
|
@Serializable
|
||||||
enum class ContactStatus {
|
enum class ContactStatus {
|
||||||
@SerialName("active") Active,
|
@SerialName("active") Active,
|
||||||
@SerialName("deleted") Deleted;
|
@SerialName("deleted") Deleted,
|
||||||
|
@SerialName("deletedByUser") DeletedByUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -1276,6 +1286,7 @@ data class GroupInfo (
|
|||||||
override val id get() = "#$groupId"
|
override val id get() = "#$groupId"
|
||||||
override val apiId get() = groupId
|
override val apiId get() = groupId
|
||||||
override val ready get() = membership.memberActive
|
override val ready get() = membership.memberActive
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val sendMsgEnabled get() = membership.memberActive
|
override val sendMsgEnabled get() = membership.memberActive
|
||||||
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
||||||
override val incognito get() = membership.memberIncognito
|
override val incognito get() = membership.memberIncognito
|
||||||
@@ -1581,6 +1592,7 @@ class NoteFolder(
|
|||||||
override val chatType get() = ChatType.Local
|
override val chatType get() = ChatType.Local
|
||||||
override val id get() = "*$noteFolderId"
|
override val id get() = "*$noteFolderId"
|
||||||
override val apiId get() = noteFolderId
|
override val apiId get() = noteFolderId
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val ready get() = true
|
override val ready get() = true
|
||||||
override val sendMsgEnabled get() = true
|
override val sendMsgEnabled get() = true
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
@@ -1617,6 +1629,7 @@ class UserContactRequest (
|
|||||||
override val chatType get() = ChatType.ContactRequest
|
override val chatType get() = ChatType.ContactRequest
|
||||||
override val id get() = "<@$contactRequestId"
|
override val id get() = "<@$contactRequestId"
|
||||||
override val apiId get() = contactRequestId
|
override val apiId get() = contactRequestId
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val ready get() = true
|
override val ready get() = true
|
||||||
override val sendMsgEnabled get() = false
|
override val sendMsgEnabled get() = false
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
@@ -1656,6 +1669,7 @@ class PendingContactConnection(
|
|||||||
override val chatType get() = ChatType.ContactConnection
|
override val chatType get() = ChatType.ContactConnection
|
||||||
override val id get () = ":$pccConnId"
|
override val id get () = ":$pccConnId"
|
||||||
override val apiId get() = pccConnId
|
override val apiId get() = pccConnId
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val ready get() = false
|
override val ready get() = false
|
||||||
override val sendMsgEnabled get() = false
|
override val sendMsgEnabled get() = false
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
|
|||||||
+45
-12
@@ -204,6 +204,8 @@ class AppPreferences {
|
|||||||
|
|
||||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||||
|
|
||||||
|
val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true)
|
||||||
|
val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true)
|
||||||
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
||||||
|
|
||||||
|
|
||||||
@@ -380,6 +382,8 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||||
|
private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||||
|
private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||||
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||||
|
|
||||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||||
@@ -1108,16 +1112,16 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteChat(chat: Chat, notify: Boolean? = null) {
|
suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||||
val cInfo = chat.chatInfo
|
val cInfo = chat.chatInfo
|
||||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) {
|
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) {
|
||||||
chatModel.removeChat(chat.remoteHostId, cInfo.id)
|
chatModel.removeChat(chat.remoteHostId, cInfo.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean {
|
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean {
|
||||||
chatModel.deletedChats.value += rh to type.type + id
|
chatModel.deletedChats.value += rh to type.type + id
|
||||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify))
|
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||||
val success = when {
|
val success = when {
|
||||||
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
||||||
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
||||||
@@ -1138,6 +1142,22 @@ object ChatController {
|
|||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? {
|
||||||
|
val type = ChatType.Direct
|
||||||
|
chatModel.deletedChats.value += rh to type.type + id
|
||||||
|
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||||
|
val contact = when {
|
||||||
|
r is CR.ContactDeleted -> r.contact
|
||||||
|
else -> {
|
||||||
|
val titleId = MR.strings.error_deleting_contact
|
||||||
|
apiErrorAlert("apiDeleteChat", generalGetString(titleId), r)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chatModel.deletedChats.value -= rh to type.type + id
|
||||||
|
return contact
|
||||||
|
}
|
||||||
|
|
||||||
fun clearChat(chat: Chat, close: (() -> Unit)? = null) {
|
fun clearChat(chat: Chat, close: (() -> Unit)? = null) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
||||||
@@ -1876,6 +1896,10 @@ object ChatController {
|
|||||||
val cInfo = r.chatItem.chatInfo
|
val cInfo = r.chatItem.chatInfo
|
||||||
val cItem = r.chatItem.chatItem
|
val cItem = r.chatItem.chatItem
|
||||||
if (active(r.user)) {
|
if (active(r.user)) {
|
||||||
|
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
|
||||||
|
val updatedContact = cInfo.contact.copy(chatDeleted = false)
|
||||||
|
chatModel.updateContact(rhId, updatedContact)
|
||||||
|
}
|
||||||
chatModel.addChatItem(rhId, cInfo, cItem)
|
chatModel.addChatItem(rhId, cInfo, cItem)
|
||||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||||
@@ -2585,7 +2609,7 @@ sealed class CC {
|
|||||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||||
class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC()
|
class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC()
|
||||||
class ApiClearChat(val type: ChatType, val id: Long): CC()
|
class ApiClearChat(val type: ChatType, val id: Long): CC()
|
||||||
class ApiListContacts(val userId: Long): CC()
|
class ApiListContacts(val userId: Long): CC()
|
||||||
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
||||||
@@ -2732,11 +2756,7 @@ sealed class CC {
|
|||||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||||
is ApiDeleteChat -> if (notify != null) {
|
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
|
||||||
"/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
|
|
||||||
} else {
|
|
||||||
"/_delete ${chatRef(type, id)}"
|
|
||||||
}
|
|
||||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
||||||
is ApiListContacts -> "/_contacts $userId"
|
is ApiListContacts -> "/_contacts $userId"
|
||||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||||
@@ -2949,8 +2969,6 @@ sealed class CC {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOff(b: Boolean): String = if (b) "on" else "off"
|
|
||||||
|
|
||||||
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -2960,6 +2978,8 @@ sealed class CC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NewUser(
|
data class NewUser(
|
||||||
val profile: Profile?,
|
val profile: Profile?,
|
||||||
@@ -4781,6 +4801,19 @@ fun chatError(r: CR): ChatErrorType? {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class ChatDeleteMode {
|
||||||
|
@Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode()
|
||||||
|
@Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode()
|
||||||
|
@Serializable @SerialName("messages") class Messages: ChatDeleteMode()
|
||||||
|
|
||||||
|
val cmdString: String get() = when (this) {
|
||||||
|
is ChatDeleteMode.Full -> "full notify=${onOff(notify)}"
|
||||||
|
is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}"
|
||||||
|
is ChatDeleteMode.Messages -> "messages"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class ConnectionPlan {
|
sealed class ConnectionPlan {
|
||||||
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
||||||
|
|||||||
+301
-33
@@ -12,12 +12,14 @@ import SectionView
|
|||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.*
|
import androidx.compose.foundation.text.*
|
||||||
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.text.*
|
import androidx.compose.ui.text.*
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
@@ -35,7 +37,8 @@ import chat.simplex.common.ui.theme.*
|
|||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.usersettings.*
|
import chat.simplex.common.views.usersettings.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.chatlist.updateChatSettings
|
import chat.simplex.common.views.call.CallMediaType
|
||||||
|
import chat.simplex.common.views.chatlist.*
|
||||||
import chat.simplex.common.views.newchat.*
|
import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -45,9 +48,21 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
sealed class ContactDeleteMode {
|
||||||
|
class Full: ContactDeleteMode()
|
||||||
|
class Entity: ContactDeleteMode()
|
||||||
|
|
||||||
|
fun toChatDeleteMode(notify: Boolean): ChatDeleteMode =
|
||||||
|
when (this) {
|
||||||
|
is Full -> ChatDeleteMode.Full(notify)
|
||||||
|
is Entity -> ChatDeleteMode.Entity(notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInfoView(
|
fun ChatInfoView(
|
||||||
chatModel: ChatModel,
|
chatModel: ChatModel,
|
||||||
|
openedFromChatView: Boolean,
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
connectionStats: ConnectionStats?,
|
connectionStats: ConnectionStats?,
|
||||||
customUserProfile: Profile?,
|
customUserProfile: Profile?,
|
||||||
@@ -68,6 +83,7 @@ fun ChatInfoView(
|
|||||||
val chatRh = chat.remoteHostId
|
val chatRh = chat.remoteHostId
|
||||||
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
||||||
ChatInfoLayout(
|
ChatInfoLayout(
|
||||||
|
openedFromChatView = openedFromChatView,
|
||||||
chat,
|
chat,
|
||||||
contact,
|
contact,
|
||||||
currentUser,
|
currentUser,
|
||||||
@@ -166,7 +182,8 @@ fun ChatInfoView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
close = close,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,53 +218,127 @@ sealed class SendReceipts {
|
|||||||
|
|
||||||
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||||
val chatInfo = chat.chatInfo
|
val chatInfo = chat.chatInfo
|
||||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
if (chatInfo is ChatInfo.Direct) {
|
||||||
title = generalGetString(MR.strings.delete_contact_question),
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
|
title = generalGetString(MR.strings.delete_contact_question),
|
||||||
buttons = {
|
buttons = {
|
||||||
Column {
|
Column {
|
||||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) {
|
// Delete contact
|
||||||
// Delete and notify contact
|
|
||||||
SectionItemView({
|
SectionItemView({
|
||||||
AlertManager.shared.hideAlert()
|
AlertManager.shared.hideAlert()
|
||||||
deleteContact(chat, chatModel, close, notify = true)
|
notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Full())
|
||||||
}) {
|
}) {
|
||||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
}
|
}
|
||||||
// Delete
|
if (!chatInfo.contact.chatDeleted) {
|
||||||
|
// Delete contact, keep conversation
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Entity())
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.delete_contact_keep_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cancel
|
||||||
SectionItemView({
|
SectionItemView({
|
||||||
AlertManager.shared.hideAlert()
|
AlertManager.shared.hideAlert()
|
||||||
deleteContact(chat, chatModel, close, notify = false)
|
|
||||||
}) {
|
}) {
|
||||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Delete
|
|
||||||
SectionItemView({
|
|
||||||
AlertManager.shared.hideAlert()
|
|
||||||
deleteContact(chat, chatModel, close)
|
|
||||||
}) {
|
|
||||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cancel
|
|
||||||
SectionItemView({
|
|
||||||
AlertManager.shared.hideAlert()
|
|
||||||
}) {
|
|
||||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyDeleteContactDialog(
|
||||||
|
chat: Chat,
|
||||||
|
chatModel: ChatModel,
|
||||||
|
close: (() -> Unit)? = null,
|
||||||
|
contactDeleteMode: ContactDeleteMode = ContactDeleteMode.Full()
|
||||||
|
) {
|
||||||
|
val chatInfo = chat.chatInfo
|
||||||
|
if (chatInfo is ChatInfo.Direct) {
|
||||||
|
val contactActive = chatInfo.contact.ready && chatInfo.contact.active
|
||||||
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
|
title = if (contactActive) generalGetString(MR.strings.notify_delete_contact_question) else generalGetString(MR.strings.confirm_delete_contact_question),
|
||||||
|
text = when (contactDeleteMode) {
|
||||||
|
is ContactDeleteMode.Full -> generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)
|
||||||
|
is ContactDeleteMode.Entity -> generalGetString(MR.strings.delete_contact_cannot_undo_warning)
|
||||||
|
},
|
||||||
|
buttons = {
|
||||||
|
Column {
|
||||||
|
if (contactActive) {
|
||||||
|
// Delete and notify contact
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = true))
|
||||||
|
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||||
|
showDeleteContactNotice(chatInfo.contact)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
// Delete without notification
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false))
|
||||||
|
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||||
|
showDeleteContactNotice(chatInfo.contact)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Delete
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false))
|
||||||
|
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||||
|
showDeleteContactNotice(chatInfo.contact)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cancel
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteContactNotice(contact: Contact) {
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = generalGetString(MR.strings.contact_deleted),
|
||||||
|
text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName),
|
||||||
|
confirmText = generalGetString(MR.strings.ok),
|
||||||
|
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||||
|
onDismiss = {
|
||||||
|
chatModel.controller.appPrefs.showDeleteContactNotice.set(false)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
|
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||||
val chatInfo = chat.chatInfo
|
val chatInfo = chat.chatInfo
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val chatRh = chat.remoteHostId
|
val chatRh = chat.remoteHostId
|
||||||
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
|
val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode)
|
||||||
if (r) {
|
if (ct != null) {
|
||||||
chatModel.removeChat(chatRh, chatInfo.id)
|
when (chatDeleteMode) {
|
||||||
|
is ChatDeleteMode.Full ->
|
||||||
|
chatModel.removeChat(chatRh, chatInfo.id)
|
||||||
|
is ChatDeleteMode.Entity ->
|
||||||
|
chatModel.updateContact(chatRh, ct)
|
||||||
|
is ChatDeleteMode.Messages ->
|
||||||
|
chatModel.clearChat(chatRh, ChatInfo.Direct(ct))
|
||||||
|
}
|
||||||
if (chatModel.chatId.value == chatInfo.id) {
|
if (chatModel.chatId.value == chatInfo.id) {
|
||||||
chatModel.chatId.value = null
|
chatModel.chatId.value = null
|
||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
@@ -280,6 +371,7 @@ fun clearNoteFolderDialog(chat: Chat, close: (() -> Unit)? = null) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInfoLayout(
|
fun ChatInfoLayout(
|
||||||
|
openedFromChatView: Boolean,
|
||||||
chat: Chat,
|
chat: Chat,
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
@@ -300,6 +392,7 @@ fun ChatInfoLayout(
|
|||||||
syncContactConnection: () -> Unit,
|
syncContactConnection: () -> Unit,
|
||||||
syncContactConnectionForce: () -> Unit,
|
syncContactConnectionForce: () -> Unit,
|
||||||
verifyClicked: () -> Unit,
|
verifyClicked: () -> Unit,
|
||||||
|
close: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val cStats = connStats.value
|
val cStats = connStats.value
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@@ -319,7 +412,31 @@ fun ChatInfoLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
||||||
|
|
||||||
SectionSpacer()
|
SectionSpacer()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (contact.activeConn == null && contact.profile.contactLink != null && contact.active) {
|
||||||
|
ConnectButton(openedFromChatView, chat, contact, close)
|
||||||
|
} else if (!contact.active && !contact.chatDeleted) {
|
||||||
|
OpenButton(openedFromChatView, chat, contact, close)
|
||||||
|
} else {
|
||||||
|
MessageButton(openedFromChatView, chat, contact, close)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
CallButton(chat, contact)
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
VideoButton(chat, contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
if (customUserProfile != null) {
|
if (customUserProfile != null) {
|
||||||
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
||||||
SectionItemViewSpaceBetween {
|
SectionItemViewSpaceBetween {
|
||||||
@@ -535,6 +652,155 @@ fun LocalAliasEditor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when contact is a "contact card"
|
||||||
|
@Composable
|
||||||
|
private fun ConnectButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||||
|
InfoViewActionButton(
|
||||||
|
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||||
|
title = generalGetString(MR.strings.info_view_connect_button),
|
||||||
|
disabled = false,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||||
|
title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName),
|
||||||
|
buttons = {
|
||||||
|
Column {
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.privacySensitive.hideAlert()
|
||||||
|
infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = false, close)
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.privacySensitive.hideAlert()
|
||||||
|
infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = true, close)
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.privacySensitive.hideAlert()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hostDevice = hostDevice(chat.remoteHostId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun infoConnectContactViaAddress(openedFromChatView: Boolean, chat: Chat, contact: Contact, incognito: Boolean, close: () -> Unit) {
|
||||||
|
withBGApi {
|
||||||
|
val ok = connectContactViaAddress(chatModel, chat.remoteHostId, contact.contactId, incognito = incognito)
|
||||||
|
if (ok) {
|
||||||
|
if (openedFromChatView) {
|
||||||
|
close.invoke()
|
||||||
|
} else {
|
||||||
|
if (contact.chatDeleted) {
|
||||||
|
chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false))
|
||||||
|
}
|
||||||
|
close.invoke()
|
||||||
|
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OpenButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||||
|
InfoViewActionButton(
|
||||||
|
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||||
|
title = generalGetString(MR.strings.info_view_open_button),
|
||||||
|
disabled = false,
|
||||||
|
onClick = {
|
||||||
|
if (openedFromChatView) {
|
||||||
|
close.invoke()
|
||||||
|
} else {
|
||||||
|
close.invoke()
|
||||||
|
withBGApi {
|
||||||
|
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||||
|
InfoViewActionButton(
|
||||||
|
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||||
|
title = generalGetString(MR.strings.info_view_message_button),
|
||||||
|
disabled = !contact.sendMsgEnabled,
|
||||||
|
onClick = {
|
||||||
|
if (openedFromChatView) {
|
||||||
|
close.invoke()
|
||||||
|
} else {
|
||||||
|
if (contact.chatDeleted) {
|
||||||
|
chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false))
|
||||||
|
}
|
||||||
|
close.invoke()
|
||||||
|
withBGApi {
|
||||||
|
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CallButton(chat: Chat, contact: Contact) {
|
||||||
|
InfoViewActionButton(
|
||||||
|
icon = painterResource(MR.images.ic_call_filled),
|
||||||
|
title = generalGetString(MR.strings.info_view_call_button),
|
||||||
|
disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null,
|
||||||
|
onClick = {
|
||||||
|
startChatCall(chat, CallMediaType.Audio)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoButton(chat: Chat, contact: Contact) {
|
||||||
|
InfoViewActionButton(
|
||||||
|
icon = painterResource(MR.images.ic_videocam_filled),
|
||||||
|
title = generalGetString(MR.strings.info_view_video_button),
|
||||||
|
disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null,
|
||||||
|
onClick = {
|
||||||
|
startChatCall(chat, CallMediaType.Video)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, onClick: () -> Unit) {
|
||||||
|
Surface(
|
||||||
|
Modifier
|
||||||
|
.width(96.dp)
|
||||||
|
.height(66.dp),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
color = MaterialTheme.colors.secondaryVariant,
|
||||||
|
) {
|
||||||
|
val modifier = if (disabled) Modifier else Modifier.clickable { onClick () }
|
||||||
|
Column(
|
||||||
|
modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.size(26.dp),
|
||||||
|
tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal),
|
||||||
|
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||||
Row(
|
Row(
|
||||||
@@ -823,6 +1089,7 @@ fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
|
|||||||
fun PreviewChatInfoLayout() {
|
fun PreviewChatInfoLayout() {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
ChatInfoLayout(
|
ChatInfoLayout(
|
||||||
|
openedFromChatView = false,
|
||||||
chat = Chat(
|
chat = Chat(
|
||||||
remoteHostId = null,
|
remoteHostId = null,
|
||||||
chatInfo = ChatInfo.Direct.sampleData,
|
chatInfo = ChatInfo.Direct.sampleData,
|
||||||
@@ -847,6 +1114,7 @@ fun PreviewChatInfoLayout() {
|
|||||||
syncContactConnection = {},
|
syncContactConnection = {},
|
||||||
syncContactConnectionForce = {},
|
syncContactConnectionForce = {},
|
||||||
verifyClicked = {},
|
verifyClicked = {},
|
||||||
|
close = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-13
@@ -189,7 +189,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||||
preloadedCode = code
|
preloadedCode = code
|
||||||
}
|
}
|
||||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
ChatInfoView(chatModel, openedFromChatView = true, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||||
KeyChangeEffect(chat.id) {
|
KeyChangeEffect(chat.id) {
|
||||||
@@ -306,18 +306,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
onComplete.invoke()
|
onComplete.invoke()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startCall = out@{ media ->
|
startCall = out@{ media -> startChatCall(chat, media) },
|
||||||
withBGApi {
|
|
||||||
val cInfo = chat.chatInfo
|
|
||||||
if (cInfo is ChatInfo.Direct) {
|
|
||||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
|
||||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
|
||||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
|
||||||
chatModel.showCallView.value = true
|
|
||||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
endCall = {
|
endCall = {
|
||||||
val call = chatModel.activeCall.value
|
val call = chatModel.activeCall.value
|
||||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||||
@@ -521,6 +510,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startChatCall(chat: Chat, media: CallMediaType) {
|
||||||
|
withBGApi {
|
||||||
|
val cInfo = chat.chatInfo
|
||||||
|
if (cInfo is ChatInfo.Direct) {
|
||||||
|
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||||
|
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||||
|
chatModel.activeCall.value = Call(remoteHostId = chat.remoteHostId, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||||
|
chatModel.showCallView.value = true
|
||||||
|
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatLayout(
|
fun ChatLayout(
|
||||||
chat: Chat,
|
chat: Chat,
|
||||||
|
|||||||
+37
-17
@@ -36,6 +36,7 @@ import chat.simplex.common.views.newchat.*
|
|||||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||||
import chat.simplex.common.model.GroupInfo
|
import chat.simplex.common.model.GroupInfo
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.views.call.CallMediaType
|
||||||
import chat.simplex.common.views.chatlist.openLoadedChat
|
import chat.simplex.common.views.chatlist.openLoadedChat
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
@@ -246,10 +247,10 @@ fun GroupMemberInfoLayout(
|
|||||||
verifyClicked: () -> Unit,
|
verifyClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val cStats = connStats.value
|
val cStats = connStats.value
|
||||||
fun knownDirectChat(contactId: Long): Chat? {
|
fun knownDirectChat(contactId: Long): Pair<Chat, Contact>? {
|
||||||
val chat = getContactChat(contactId)
|
val chat = getContactChat(contactId)
|
||||||
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||||
chat
|
chat to chat.chatInfo.contact
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -309,17 +310,37 @@ fun GroupMemberInfoLayout(
|
|||||||
|
|
||||||
val contactId = member.memberContactId
|
val contactId = member.memberContactId
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
|
||||||
|
if (knownChat != null) {
|
||||||
|
val (chat, contact) = knownChat
|
||||||
|
OpenChatButton(onClick = { openDirectChat(contact.contactId) })
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
CallButton(chat, contact)
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
VideoButton(chat, contact)
|
||||||
|
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||||
|
if (contactId != null) {
|
||||||
|
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||||
|
} else {
|
||||||
|
OpenChatButton(onClick = { createMemberContact() })
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
InfoViewActionButton(painterResource(MR.images.ic_call_filled), generalGetString(MR.strings.info_view_call_button), disabled = true, onClick = {})
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
InfoViewActionButton(painterResource(MR.images.ic_videocam_filled), generalGetString(MR.strings.info_view_video_button), disabled = true, onClick = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
if (member.memberActive) {
|
if (member.memberActive) {
|
||||||
SectionView {
|
SectionView {
|
||||||
if (contactId != null && knownDirectChat(contactId) != null) {
|
|
||||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
|
||||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
|
||||||
if (contactId != null) {
|
|
||||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
|
||||||
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
|
|
||||||
OpenChatButton(onClick = { createMemberContact() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (connectionCode != null) {
|
if (connectionCode != null) {
|
||||||
VerifyCodeButton(member.verified, verifyClicked)
|
VerifyCodeButton(member.verified, verifyClicked)
|
||||||
}
|
}
|
||||||
@@ -513,12 +534,11 @@ fun RemoveMemberButton(onClick: () -> Unit) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OpenChatButton(onClick: () -> Unit) {
|
fun OpenChatButton(onClick: () -> Unit) {
|
||||||
SettingsActionItem(
|
InfoViewActionButton(
|
||||||
painterResource(MR.images.ic_chat),
|
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||||
stringResource(MR.strings.button_send_direct_message),
|
title = generalGetString(MR.strings.info_view_message_button),
|
||||||
click = onClick,
|
disabled = false,
|
||||||
textColor = MaterialTheme.colors.primary,
|
onClick = onClick
|
||||||
iconColor = MaterialTheme.colors.primary,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-2
@@ -172,7 +172,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ErrorChatListItem() {
|
fun ErrorChatListItem() {
|
||||||
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
||||||
Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
@@ -407,13 +407,73 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
|
|||||||
stringResource(MR.strings.delete_contact_menu_action),
|
stringResource(MR.strings.delete_contact_menu_action),
|
||||||
painterResource(MR.images.ic_delete),
|
painterResource(MR.images.ic_delete),
|
||||||
onClick = {
|
onClick = {
|
||||||
deleteContactDialog(chat, chatModel)
|
deleteContactConversationDialog(chat, chatModel)
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
},
|
},
|
||||||
color = Color.Red
|
color = Color.Red
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteContactConversationDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||||
|
val chatInfo = chat.chatInfo
|
||||||
|
if (chatInfo is ChatInfo.Direct) {
|
||||||
|
val contactDeletedByUser = chatInfo.contact.contactStatus == ContactStatus.DeletedByUser
|
||||||
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
|
title = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_question) else generalGetString(MR.strings.delete_contact_question),
|
||||||
|
text = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_all_messages_deleted_cannot_undo_warning) else null,
|
||||||
|
buttons = {
|
||||||
|
Column {
|
||||||
|
if (contactDeletedByUser) {
|
||||||
|
// Delete conversation
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Full(notify = false))
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Delete contact
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
notifyDeleteContactDialog(chat, chatModel, close)
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
// Only delete conversation
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages())
|
||||||
|
if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) {
|
||||||
|
showDeleteConversationNotice(chatInfo.contact)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cancel
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteConversationNotice(contact: Contact) {
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = generalGetString(MR.strings.conversation_deleted),
|
||||||
|
text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName),
|
||||||
|
confirmText = generalGetString(MR.strings.ok),
|
||||||
|
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||||
|
onDismiss = {
|
||||||
|
chatModel.controller.appPrefs.showDeleteConversationNotice.set(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||||
ItemAction(
|
ItemAction(
|
||||||
|
|||||||
+15
-331
@@ -3,336 +3,46 @@ package chat.simplex.common.views.chatlist
|
|||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
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.runtime.*
|
import androidx.compose.runtime.*
|
||||||
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.focus.*
|
import androidx.compose.ui.focus.*
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
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.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.SettingsViewState
|
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
|
||||||
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.WhatsNewView
|
|
||||||
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
|
||||||
import chat.simplex.common.views.usersettings.SettingsView
|
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.call.Call
|
|
||||||
import chat.simplex.common.views.newchat.*
|
import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
fun ToggleFilterButton() {
|
||||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
|
||||||
val showNewChatSheet = {
|
|
||||||
newChatSheetState.value = AnimatedViewState.VISIBLE
|
|
||||||
}
|
|
||||||
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
|
|
||||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
|
||||||
else newChatSheetState.value = AnimatedViewState.GONE
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (shouldShowWhatsNew(chatModel)) {
|
|
||||||
delay(1000L)
|
|
||||||
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
|
||||||
if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false)
|
|
||||||
}
|
|
||||||
if (appPlatform.isDesktop) {
|
|
||||||
KeyChangeEffect(chatModel.chatId.value) {
|
|
||||||
if (chatModel.chatId.value != null) {
|
|
||||||
ModalManager.end.closeModalsExceptFirst()
|
|
||||||
}
|
|
||||||
AudioPlayer.stop()
|
|
||||||
VideoPlayerHolder.stopAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
|
||||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val (userPickerState, scaffoldState ) = settingsState
|
|
||||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(searchText, scaffoldState.drawerState, userPickerState, stopped)} },
|
|
||||||
scaffoldState = scaffoldState,
|
|
||||||
drawerContent = {
|
|
||||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
|
||||||
SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentColor = LocalContentColor.current,
|
|
||||||
drawerContentColor = LocalContentColor.current,
|
|
||||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
|
||||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
|
||||||
floatingActionButton = {
|
|
||||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
if (!stopped) {
|
|
||||||
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp),
|
|
||||||
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(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box(Modifier.padding(it).padding(end = endPadding)) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
if (!chatModel.desktopNoUserNoRemote) {
|
|
||||||
ChatList(chatModel, searchText = searchText)
|
|
||||||
}
|
|
||||||
if (chatModel.chats.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 (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true && searchText.value.text.isEmpty()) {
|
|
||||||
OnboardingButtons(showNewChatSheet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (searchText.value.text.isEmpty()) {
|
|
||||||
if (appPlatform.isDesktop) {
|
|
||||||
val call = remember { chatModel.activeCall }.value
|
|
||||||
if (call != null) {
|
|
||||||
ActiveCallInteractiveArea(call, newChatSheetState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO disable this button and sheet for the duration of the switch
|
|
||||||
tryOrShowError("NewChatSheet", error = {}) {
|
|
||||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (appPlatform.isAndroid) {
|
|
||||||
tryOrShowError("UserPicker", error = {}) {
|
|
||||||
UserPicker(chatModel, userPickerState) {
|
|
||||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
|
||||||
userPickerState.value = AnimatedViewState.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
|
|
||||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
|
|
||||||
ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet)
|
|
||||||
val color = MaterialTheme.colors.primaryVariant
|
|
||||||
Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = {
|
|
||||||
val trianglePath = Path().apply {
|
|
||||||
moveTo(0.dp.toPx(), 0f)
|
|
||||||
lineTo(16.dp.toPx(), 0.dp.toPx())
|
|
||||||
lineTo(8.dp.toPx(), 10.dp.toPx())
|
|
||||||
lineTo(0.dp.toPx(), 0.dp.toPx())
|
|
||||||
}
|
|
||||||
drawPath(
|
|
||||||
color = color,
|
|
||||||
path = trianglePath
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Spacer(Modifier.height(62.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ConnectButton(text: String, onClick: () -> Unit) {
|
|
||||||
Button(
|
|
||||||
onClick,
|
|
||||||
shape = RoundedCornerShape(21.dp),
|
|
||||||
colors = ButtonDefaults.textButtonColors(
|
|
||||||
backgroundColor = MaterialTheme.colors.primaryVariant
|
|
||||||
),
|
|
||||||
elevation = null,
|
|
||||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF),
|
|
||||||
modifier = Modifier.height(42.dp)
|
|
||||||
) {
|
|
||||||
Text(text, color = Color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
|
||||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
|
||||||
if (stopped) {
|
|
||||||
barButtons.add {
|
|
||||||
IconButton(onClick = {
|
|
||||||
AlertManager.shared.showAlertMsg(
|
|
||||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
|
||||||
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
painterResource(MR.images.ic_report_filled),
|
|
||||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
|
||||||
tint = Color.Red,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
DefaultTopAppBar(
|
|
||||||
navigationButton = {
|
|
||||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
|
||||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
|
||||||
} else {
|
|
||||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
|
||||||
val allRead = users
|
|
||||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
|
||||||
.all { u -> u.unreadCount == 0 }
|
|
||||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
|
||||||
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
|
|
||||||
scope.launch { drawerState.open() }
|
|
||||||
} else {
|
|
||||||
userPickerState.value = AnimatedViewState.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
stringResource(MR.strings.your_chats),
|
|
||||||
color = MaterialTheme.colors.onBackground,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
|
||||||
if (chatModel.chats.size > 0) {
|
|
||||||
val enabled = remember { derivedStateOf { searchInList.value.text.isEmpty() } }
|
|
||||||
if (enabled.value) {
|
|
||||||
ToggleFilterEnabledButton()
|
|
||||||
} else {
|
|
||||||
ToggleFilterDisabledButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTitleClick = null,
|
|
||||||
showSearch = false,
|
|
||||||
onSearchValueChanged = {},
|
|
||||||
buttons = barButtons
|
|
||||||
)
|
|
||||||
Divider(Modifier.padding(top = AppBarHeight))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
IconButton(onClick = onButtonClicked) {
|
|
||||||
Box {
|
|
||||||
ProfileImage(
|
|
||||||
image = image,
|
|
||||||
size = 37.dp,
|
|
||||||
color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
|
||||||
)
|
|
||||||
if (!allRead) {
|
|
||||||
unreadBadge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (appPlatform.isDesktop) {
|
|
||||||
val h by remember { chatModel.currentRemoteHost }
|
|
||||||
if (h != null) {
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
HostDisconnectButton {
|
|
||||||
stopRemoteHostAndReloadHosts(h!!, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BoxScope.unreadBadge(text: String? = "") {
|
|
||||||
Text(
|
|
||||||
text ?: "",
|
|
||||||
color = MaterialTheme.colors.onPrimary,
|
|
||||||
fontSize = 6.sp,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
|
||||||
.badgeLayout()
|
|
||||||
.padding(horizontal = 3.dp)
|
|
||||||
.padding(vertical = 1.dp)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ToggleFilterEnabledButton() {
|
|
||||||
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
||||||
IconButton(onClick = { pref.set(!pref.get()) }) {
|
IconButton(onClick = { pref.set(!pref.get()) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(MR.images.ic_filter_list),
|
painterResource(MR.images.ic_filter_list),
|
||||||
null,
|
null,
|
||||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.primary,
|
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(3.dp)
|
.padding(3.dp)
|
||||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||||
.border(width = 1.dp, color = MaterialTheme.colors.primary, shape = RoundedCornerShape(50))
|
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||||
.padding(3.dp)
|
.padding(3.dp)
|
||||||
.size(16.dp)
|
.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ToggleFilterDisabledButton() {
|
|
||||||
IconButton({}, enabled = false) {
|
|
||||||
Icon(
|
|
||||||
painterResource(MR.images.ic_filter_list),
|
|
||||||
null,
|
|
||||||
tint = MaterialTheme.colors.secondary,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(3.dp)
|
|
||||||
.border(width = 1.dp, color = MaterialTheme.colors.secondary, shape = RoundedCornerShape(50))
|
|
||||||
.padding(3.dp)
|
|
||||||
.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
|
||||||
|
|
||||||
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
|
||||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
|
||||||
if (chatModel.currentUser.value == null) {
|
|
||||||
chatModel.appOpenUrl.value = rhId to uri
|
|
||||||
} else {
|
|
||||||
withBGApi {
|
|
||||||
planAndConnect(rhId, uri, incognito = null, close = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
@@ -359,28 +69,8 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
|||||||
} else {
|
} else {
|
||||||
Row {
|
Row {
|
||||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||||
val clipboard = LocalClipboardManager.current
|
if (chatModel.chats.size > 0) {
|
||||||
val clipboardHasText = remember(focused) { chatModel.clipboardHasText }.value
|
ToggleFilterButton()
|
||||||
if (clipboardHasText) {
|
|
||||||
IconButton(
|
|
||||||
onClick = { searchText.value = searchText.value.copy(clipboard.getText()?.text ?: return@IconButton) },
|
|
||||||
Modifier.size(30.dp).desktopPointerHoverIconHand()
|
|
||||||
) {
|
|
||||||
Icon(painterResource(MR.images.ic_article), null, tint = MaterialTheme.colors.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(padding))
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
val fixedRhId = chatModel.currentRemoteHost.value
|
|
||||||
ModalManager.center.closeModals()
|
|
||||||
ModalManager.center.showModalCloseable { close ->
|
|
||||||
NewChatView(fixedRhId, selection = NewChatOption.CONNECT, showQRCodeScanner = true, close = close)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Modifier.size(30.dp).desktopPointerHoverIconHand()
|
|
||||||
) {
|
|
||||||
Icon(painterResource(MR.images.ic_qr_code), null, tint = MaterialTheme.colors.secondary)
|
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(padding))
|
Spacer(Modifier.width(padding))
|
||||||
}
|
}
|
||||||
@@ -437,17 +127,10 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ErrorSettingsView() {
|
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
Text(generalGetString(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lazyListState = 0 to 0
|
private var lazyListState = 0 to 0
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||||
@@ -508,17 +191,18 @@ private fun filteredChats(
|
|||||||
} else {
|
} else {
|
||||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||||
if (s.isEmpty() && !showUnreadAndFavorites)
|
if (s.isEmpty() && !showUnreadAndFavorites)
|
||||||
chats
|
chats.filter { chat -> !chat.chatInfo.chatDeleted }
|
||||||
else {
|
else {
|
||||||
chats.filter { chat ->
|
chats.filter { chat ->
|
||||||
when (val cInfo = chat.chatInfo) {
|
when (val cInfo = chat.chatInfo) {
|
||||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
is ChatInfo.Direct -> !chat.chatInfo.chatDeleted && (
|
||||||
chat.id == chatModel.chatId.value || filtered(chat)
|
if (s.isEmpty()) {
|
||||||
} else {
|
chat.id == chatModel.chatId.value || filtered(chat)
|
||||||
(viewNameContains(cInfo, s) ||
|
} else {
|
||||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
(viewNameContains(cInfo, s) ||
|
||||||
cInfo.contact.fullName.lowercase().contains(s))
|
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||||
}
|
cInfo.contact.fullName.lowercase().contains(s))
|
||||||
|
})
|
||||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-1
@@ -198,7 +198,7 @@ fun ChatPreviewView(
|
|||||||
} else {
|
} else {
|
||||||
when (cInfo) {
|
when (cInfo) {
|
||||||
is ChatInfo.Direct ->
|
is ChatInfo.Direct ->
|
||||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) {
|
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) {
|
||||||
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
|
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
|
||||||
} else if (!cInfo.ready && cInfo.contact.activeConn != null) {
|
} else if (!cInfo.ready && cInfo.contact.activeConn != null) {
|
||||||
if (cInfo.contact.nextSendGrpInv) {
|
if (cInfo.contact.nextSendGrpInv) {
|
||||||
|
|||||||
+1
@@ -16,6 +16,7 @@ import chat.simplex.common.SettingsViewState
|
|||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.views.home.UserProfileButton
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
|||||||
+2
@@ -153,6 +153,8 @@ fun UserPicker(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(bottom = BottomAppBarHeight)
|
||||||
.widthIn(min = 260.dp)
|
.widthIn(min = 260.dp)
|
||||||
.width(IntrinsicSize.Min)
|
.width(IntrinsicSize.Min)
|
||||||
.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package chat.simplex.common.views.contacts
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.views.chat.*
|
||||||
|
import chat.simplex.common.views.chat.item.ItemAction
|
||||||
|
import chat.simplex.common.views.chatlist.*
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||||
|
val showMenu = remember { mutableStateOf(false) }
|
||||||
|
val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)
|
||||||
|
LaunchedEffect(chat.id) {
|
||||||
|
showMenu.value = false
|
||||||
|
delay(500L)
|
||||||
|
}
|
||||||
|
val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } }
|
||||||
|
val view = LocalMultiplatformView()
|
||||||
|
|
||||||
|
when (chat.chatInfo) {
|
||||||
|
is ChatInfo.Direct -> {
|
||||||
|
ChatListNavLinkLayout(
|
||||||
|
chatLinkPreview = {
|
||||||
|
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||||
|
ContactPreviewView(chat, disabled)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
click = {
|
||||||
|
val chatRh = chat.remoteHostId
|
||||||
|
if (ModalManager.end.hasModalsOpen()) {
|
||||||
|
ModalManager.end.closeModals()
|
||||||
|
// return@ChatLayout -- TODO what's this for?
|
||||||
|
}
|
||||||
|
hideKeyboard(view)
|
||||||
|
withBGApi {
|
||||||
|
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||||
|
var preloadedCode: String? = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||||
|
ModalManager.end.showModalCloseable(true) { close ->
|
||||||
|
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||||
|
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||||
|
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||||
|
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||||
|
preloadedContactInfo = contactInfo
|
||||||
|
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||||
|
preloadedCode = code
|
||||||
|
}
|
||||||
|
ChatInfoView(chatModel, openedFromChatView = false, chat.chatInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropdownMenuItems = {
|
||||||
|
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
|
||||||
|
ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMenu,
|
||||||
|
disabled,
|
||||||
|
selectedChat,
|
||||||
|
nextChatSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||||
|
if (contact.activeConn != null) {
|
||||||
|
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||||
|
}
|
||||||
|
DeleteContactAction(chat, chatModel, showMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) {
|
||||||
|
ItemAction(
|
||||||
|
if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat),
|
||||||
|
if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star),
|
||||||
|
onClick = {
|
||||||
|
toggleChatFavorite(chat, !favorite, chatModel)
|
||||||
|
showMenu.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.delete_contact_menu_action),
|
||||||
|
painterResource(MR.images.ic_delete),
|
||||||
|
onClick = {
|
||||||
|
deleteContactDialog(chat, chatModel)
|
||||||
|
showMenu.value = false
|
||||||
|
},
|
||||||
|
color = Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
package chat.simplex.common.views.contacts
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.chatModel
|
||||||
|
import chat.simplex.common.ui.theme.DEFAULT_SPACE_AFTER_ICON
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactPreviewView(
|
||||||
|
chat: Chat,
|
||||||
|
disabled: Boolean,
|
||||||
|
) {
|
||||||
|
val cInfo = chat.chatInfo
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun inactiveIcon() {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_cancel_filled),
|
||||||
|
stringResource(MR.strings.icon_descr_group_inactive),
|
||||||
|
Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape),
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun chatPreviewImageOverlayIcon() {
|
||||||
|
when (cInfo) {
|
||||||
|
is ChatInfo.Direct ->
|
||||||
|
if (!cInfo.contact.active) {
|
||||||
|
inactiveIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerifiedIcon() {
|
||||||
|
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun chatPreviewTitle() {
|
||||||
|
val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) }
|
||||||
|
when (cInfo) {
|
||||||
|
is ChatInfo.Direct ->
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (cInfo.contact.verified) {
|
||||||
|
VerifiedIcon()
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
cInfo.chatViewName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = if (deleting) MaterialTheme.colors.secondary else Color.Unspecified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.BottomEnd) {
|
||||||
|
ChatInfoImage(cInfo, size = 42.dp)
|
||||||
|
Box(Modifier.padding(end = 2.dp, bottom = 2.dp)) {
|
||||||
|
chatPreviewImageOverlayIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||||
|
|
||||||
|
Box(modifier = Modifier.weight(10f, fill = true)) {
|
||||||
|
chatPreviewTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||||
|
|
||||||
|
if (chat.chatInfo.chatSettings?.favorite == true) {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_star_filled),
|
||||||
|
contentDescription = generalGetString(MR.strings.favorite_chat),
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(17.dp)
|
||||||
|
)
|
||||||
|
if (chat.chatInfo.incognito) {
|
||||||
|
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat.chatInfo.incognito) {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_theater_comedy),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(21.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
package chat.simplex.common.views.contacts
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.*
|
||||||
|
import androidx.compose.ui.platform.*
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.views.chatlist.ToggleFilterButton
|
||||||
|
import chat.simplex.common.views.home.contactChats
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContactsSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var focused by remember { mutableStateOf(false) }
|
||||||
|
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary)
|
||||||
|
SearchTextField(
|
||||||
|
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||||
|
placeholder = stringResource(MR.strings.search_verb),
|
||||||
|
alwaysVisible = true,
|
||||||
|
searchText = searchText,
|
||||||
|
trailingContent = null,
|
||||||
|
) {
|
||||||
|
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 {
|
||||||
|
Row {
|
||||||
|
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||||
|
if (chatModel.chats.size > 0) {
|
||||||
|
ToggleFilterButton()
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(padding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardState = getKeyboardState()
|
||||||
|
LaunchedEffect(keyboardState.value) {
|
||||||
|
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { searchText.value.text }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lazyListState = 0 to 0
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactsList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||||
|
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||||
|
}
|
||||||
|
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||||
|
val allContactChats = remember(chatModel.chats.toList()) { contactChats(chatModel.chats) }
|
||||||
|
// See "IndexOutOfBoundsException" comment in ChatListView
|
||||||
|
val filteredContactChats = filteredContactChats(showUnreadAndFavorites, searchText.value.text, allContactChats.toList())
|
||||||
|
LazyColumnWithScrollBar(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
listState
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.offset {
|
||||||
|
val y = if (searchText.value.text.isEmpty()) {
|
||||||
|
if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
IntOffset(0, y)
|
||||||
|
}
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
) {
|
||||||
|
ContactsSearchBar(listState, searchText)
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsIndexed(filteredContactChats) { index, chat ->
|
||||||
|
val nextChatSelected = remember(chat.id, filteredContactChats) { derivedStateOf {
|
||||||
|
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||||
|
} }
|
||||||
|
ContactListNavLinkView(chat, nextChatSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filteredContactChats.isEmpty() && allContactChats.isNotEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text(generalGetString(MR.strings.no_filtered_contacts), color = MaterialTheme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filteredContactChats(
|
||||||
|
showUnreadAndFavorites: Boolean,
|
||||||
|
searchText: String,
|
||||||
|
contactChats: List<Chat>
|
||||||
|
): List<Chat> {
|
||||||
|
val s = searchText.trim().lowercase()
|
||||||
|
return (
|
||||||
|
if (s.isEmpty() && !showUnreadAndFavorites)
|
||||||
|
contactChats.filter { chat ->
|
||||||
|
when (val cInfo = chat.chatInfo) {
|
||||||
|
is ChatInfo.Direct -> cInfo.contact.contactStatus != ContactStatus.DeletedByUser
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
contactChats.filter { chat ->
|
||||||
|
when (val cInfo = chat.chatInfo) {
|
||||||
|
is ChatInfo.Direct -> cInfo.contact.contactStatus != ContactStatus.DeletedByUser && (
|
||||||
|
if (s.isEmpty()) {
|
||||||
|
chat.id == chatModel.chatId.value ||
|
||||||
|
(cInfo.chatSettings?.favorite ?: false)
|
||||||
|
} else {
|
||||||
|
(viewNameContains(cInfo, s) ||
|
||||||
|
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||||
|
cInfo.contact.fullName.lowercase().contains(s))
|
||||||
|
})
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).sortedBy { it.chatInfo.displayName.lowercase() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||||
|
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||||
+2
-1
@@ -15,7 +15,7 @@ import chat.simplex.res.MR
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultTopAppBar(
|
fun DefaultTopAppBar(
|
||||||
navigationButton: @Composable RowScope.() -> Unit,
|
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||||
title: (@Composable () -> Unit)?,
|
title: (@Composable () -> Unit)?,
|
||||||
onTitleClick: (() -> Unit)? = null,
|
onTitleClick: (() -> Unit)? = null,
|
||||||
showSearch: Boolean,
|
showSearch: Boolean,
|
||||||
@@ -125,5 +125,6 @@ private fun TopAppBar(
|
|||||||
|
|
||||||
val AppBarHeight = 56.dp
|
val AppBarHeight = 56.dp
|
||||||
val AppBarHorizontalPadding = 4.dp
|
val AppBarHorizontalPadding = 4.dp
|
||||||
|
val BottomAppBarHeight = 60.dp
|
||||||
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
|
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
|
||||||
val TitleInsetWithIcon = 72.dp
|
val TitleInsetWithIcon = 72.dp
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import androidx.compose.ui.unit.*
|
|||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.ThemeOverrides
|
import chat.simplex.common.ui.theme.ThemeOverrides
|
||||||
import chat.simplex.common.views.chatlist.connectIfOpenedViaUri
|
import chat.simplex.common.views.home.connectIfOpenedViaUri
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import com.charleskorn.kaml.decodeFromStream
|
import com.charleskorn.kaml.decodeFromStream
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
|||||||
+372
@@ -0,0 +1,372 @@
|
|||||||
|
package chat.simplex.common.views.home
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import chat.simplex.common.SettingsViewState
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.chatModel
|
||||||
|
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||||
|
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||||
|
import chat.simplex.common.views.usersettings.SettingsView
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.views.call.Call
|
||||||
|
import chat.simplex.common.views.chatlist.*
|
||||||
|
import chat.simplex.common.views.contacts.ContactsList
|
||||||
|
import chat.simplex.common.views.newchat.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
sealed class HomeTab {
|
||||||
|
class Chats: HomeTab()
|
||||||
|
class Contacts: HomeTab()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||||
|
val homeTab = remember { mutableStateOf<HomeTab>(HomeTab.Chats()) }
|
||||||
|
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||||
|
val showNewChatSheet = {
|
||||||
|
newChatSheetState.value = AnimatedViewState.VISIBLE
|
||||||
|
}
|
||||||
|
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
|
||||||
|
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||||
|
else newChatSheetState.value = AnimatedViewState.GONE
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (shouldShowWhatsNew(chatModel)) {
|
||||||
|
delay(1000L)
|
||||||
|
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||||
|
if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false)
|
||||||
|
}
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
KeyChangeEffect(chatModel.chatId.value) {
|
||||||
|
if (chatModel.chatId.value != null) {
|
||||||
|
ModalManager.end.closeModalsExceptFirst()
|
||||||
|
}
|
||||||
|
AudioPlayer.stop()
|
||||||
|
VideoPlayerHolder.stopAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||||
|
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val (userPickerState, scaffoldState ) = settingsState
|
||||||
|
Scaffold(
|
||||||
|
topBar = { Box(Modifier.padding(end = endPadding)) { HomeTopBar(homeTab, stopped) } },
|
||||||
|
bottomBar = { Box(Modifier.padding(end = endPadding)) { HomeBottomBar(scaffoldState.drawerState, userPickerState, homeTab, stopped) } },
|
||||||
|
scaffoldState = scaffoldState,
|
||||||
|
drawerContent = {
|
||||||
|
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||||
|
SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentColor = LocalContentColor.current,
|
||||||
|
drawerContentColor = LocalContentColor.current,
|
||||||
|
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||||
|
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||||
|
floatingActionButton = {
|
||||||
|
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
if (!stopped) {
|
||||||
|
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp),
|
||||||
|
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(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(Modifier.padding(it).padding(end = endPadding)) {
|
||||||
|
when (homeTab.value) {
|
||||||
|
is HomeTab.Chats -> ChatsView(searchText)
|
||||||
|
is HomeTab.Contacts -> ContactsView(searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (searchText.value.text.isEmpty()) {
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
val call = remember { chatModel.activeCall }.value
|
||||||
|
if (call != null) {
|
||||||
|
ActiveCallInteractiveArea(call, newChatSheetState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO disable this button and sheet for the duration of the switch
|
||||||
|
tryOrShowError("NewChatSheet", error = {}) {
|
||||||
|
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (appPlatform.isAndroid) {
|
||||||
|
tryOrShowError("UserPicker", error = {}) {
|
||||||
|
UserPicker(chatModel, userPickerState) {
|
||||||
|
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||||
|
userPickerState.value = AnimatedViewState.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChatsView(searchText: MutableState<TextFieldValue>) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
|
ChatList(chatModel, searchText = searchText)
|
||||||
|
}
|
||||||
|
if (chatModel.chats.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 ContactsView(searchText: MutableState<TextFieldValue>) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
|
ContactsList(chatModel, searchText = searchText)
|
||||||
|
}
|
||||||
|
if (remember(chatModel.chats.toList()) { contactChats(chatModel.chats) }.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||||
|
Text(stringResource(
|
||||||
|
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.no_contacts),
|
||||||
|
Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contactChats(c: List<Chat>): List<Chat> {
|
||||||
|
return c.filter { it.chatInfo is ChatInfo.Direct }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HomeTopBar(homeTab: MutableState<HomeTab>, stopped: Boolean) {
|
||||||
|
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||||
|
if (stopped) {
|
||||||
|
barButtons.add {
|
||||||
|
IconButton(onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||||
|
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_report_filled),
|
||||||
|
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||||
|
tint = Color.Red,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val title = when (homeTab.value) {
|
||||||
|
is HomeTab.Chats -> stringResource(MR.strings.your_chats)
|
||||||
|
is HomeTab.Contacts -> stringResource(MR.strings.contacts)
|
||||||
|
}
|
||||||
|
DefaultTopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTitleClick = null,
|
||||||
|
showSearch = false,
|
||||||
|
onSearchValueChanged = {},
|
||||||
|
buttons = barButtons
|
||||||
|
)
|
||||||
|
Divider(Modifier.padding(top = AppBarHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HomeBottomBar(
|
||||||
|
drawerState: DrawerState,
|
||||||
|
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||||
|
homeTab: MutableState<HomeTab>,
|
||||||
|
stopped: Boolean) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(BottomAppBarHeight)
|
||||||
|
.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f))
|
||||||
|
) {
|
||||||
|
Divider()
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
SettingsButton(drawerState, userPickerState, stopped)
|
||||||
|
|
||||||
|
HomeTabButton(
|
||||||
|
icon = painterResource(if (homeTab.value is HomeTab.Chats) MR.images.ic_chat_bubble_filled else MR.images.ic_chat_bubble),
|
||||||
|
title = generalGetString(MR.strings.your_chats),
|
||||||
|
onClick = { homeTab.value = HomeTab.Chats() }
|
||||||
|
)
|
||||||
|
|
||||||
|
HomeTabButton(
|
||||||
|
icon = if (homeTab.value is HomeTab.Contacts) rememberVectorPainter(AccountCircleFilled) else painterResource(MR.images.ic_account_circle_filled),
|
||||||
|
title = generalGetString(MR.strings.contacts),
|
||||||
|
onClick = { homeTab.value = HomeTab.Contacts() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsButton(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||||
|
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||||
|
} else {
|
||||||
|
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||||
|
val allRead = users
|
||||||
|
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||||
|
.all { u -> u.unreadCount == 0 }
|
||||||
|
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||||
|
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
|
||||||
|
scope.launch { drawerState.open() }
|
||||||
|
} else {
|
||||||
|
userPickerState.value = AnimatedViewState.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = onButtonClicked) {
|
||||||
|
Box {
|
||||||
|
ProfileImage(
|
||||||
|
image = image,
|
||||||
|
size = 56.dp,
|
||||||
|
color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||||
|
)
|
||||||
|
if (!allRead) {
|
||||||
|
unreadBadge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
val h by remember { chatModel.currentRemoteHost }
|
||||||
|
if (h != null) {
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
HostDisconnectButton {
|
||||||
|
stopRemoteHostAndReloadHosts(h!!, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.unreadBadge(text: String? = "") {
|
||||||
|
Text(
|
||||||
|
text ?: "",
|
||||||
|
color = MaterialTheme.colors.onPrimary,
|
||||||
|
fontSize = 6.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||||
|
.badgeLayout()
|
||||||
|
.padding(horizontal = 3.dp)
|
||||||
|
.padding(vertical = 1.dp)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeTabButton(icon: Painter, title: String, onClick: () -> Unit) {
|
||||||
|
Surface(
|
||||||
|
Modifier
|
||||||
|
.size(56.dp),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
color = Color.Transparent,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.clickable { onClick () },
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal, fontSize = 12.sp),
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||||
|
|
||||||
|
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||||
|
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||||
|
if (chatModel.currentUser.value == null) {
|
||||||
|
chatModel.appOpenUrl.value = rhId to uri
|
||||||
|
} else {
|
||||||
|
withBGApi {
|
||||||
|
planAndConnect(rhId, uri, incognito = null, close = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ErrorSettingsView() {
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text(generalGetString(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-3
@@ -44,6 +44,11 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
|||||||
ModalManager.center.closeModals()
|
ModalManager.center.closeModals()
|
||||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
|
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
|
||||||
},
|
},
|
||||||
|
scanPaste = {
|
||||||
|
closeNewChatSheet(false)
|
||||||
|
ModalManager.center.closeModals()
|
||||||
|
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
|
||||||
|
},
|
||||||
createGroup = {
|
createGroup = {
|
||||||
closeNewChatSheet(false)
|
closeNewChatSheet(false)
|
||||||
ModalManager.center.closeModals()
|
ModalManager.center.closeModals()
|
||||||
@@ -55,15 +60,17 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
|||||||
|
|
||||||
private val titles = listOf(
|
private val titles = listOf(
|
||||||
MR.strings.add_contact_tab,
|
MR.strings.add_contact_tab,
|
||||||
|
MR.strings.scan_paste_link,
|
||||||
MR.strings.create_group_button
|
MR.strings.create_group_button
|
||||||
)
|
)
|
||||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_group)
|
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewChatSheetLayout(
|
private fun NewChatSheetLayout(
|
||||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||||
stopped: Boolean,
|
stopped: Boolean,
|
||||||
addContact: () -> Unit,
|
addContact: () -> Unit,
|
||||||
|
scanPaste: () -> Unit,
|
||||||
createGroup: () -> Unit,
|
createGroup: () -> Unit,
|
||||||
closeNewChatSheet: (animated: Boolean) -> Unit,
|
closeNewChatSheet: (animated: Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -102,7 +109,7 @@ private fun NewChatSheetLayout(
|
|||||||
verticalArrangement = Arrangement.Bottom,
|
verticalArrangement = Arrangement.Bottom,
|
||||||
horizontalAlignment = Alignment.End
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
val actions = remember { listOf(addContact, createGroup) }
|
val actions = remember { listOf(addContact, scanPaste, createGroup) }
|
||||||
val backgroundColor = if (isInDarkTheme())
|
val backgroundColor = if (isInDarkTheme())
|
||||||
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
|
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
|
||||||
else
|
else
|
||||||
@@ -145,7 +152,7 @@ private fun NewChatSheetLayout(
|
|||||||
}
|
}
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { if (!stopped) closeNewChatSheet(true) },
|
onClick = { if (!stopped) closeNewChatSheet(true) },
|
||||||
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
|
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + BottomAppBarHeight),
|
||||||
elevation = FloatingActionButtonDefaults.elevation(
|
elevation = FloatingActionButtonDefaults.elevation(
|
||||||
defaultElevation = 0.dp,
|
defaultElevation = 0.dp,
|
||||||
pressedElevation = 0.dp,
|
pressedElevation = 0.dp,
|
||||||
@@ -264,6 +271,7 @@ private fun PreviewNewChatSheet() {
|
|||||||
MutableStateFlow(AnimatedViewState.VISIBLE),
|
MutableStateFlow(AnimatedViewState.VISIBLE),
|
||||||
stopped = false,
|
stopped = false,
|
||||||
addContact = {},
|
addContact = {},
|
||||||
|
scanPaste = {},
|
||||||
createGroup = {},
|
createGroup = {},
|
||||||
closeNewChatSheet = {},
|
closeNewChatSheet = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -336,6 +336,7 @@
|
|||||||
<string name="welcome">Welcome!</string>
|
<string name="welcome">Welcome!</string>
|
||||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||||
<string name="your_chats">Chats</string>
|
<string name="your_chats">Chats</string>
|
||||||
|
<string name="contacts">Contacts</string>
|
||||||
<string name="contact_connection_pending">connecting…</string>
|
<string name="contact_connection_pending">connecting…</string>
|
||||||
<string name="member_contact_send_direct_message">send direct message</string>
|
<string name="member_contact_send_direct_message">send direct message</string>
|
||||||
<string name="group_preview_you_are_invited">you are invited to group</string>
|
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||||
@@ -346,6 +347,8 @@
|
|||||||
<string name="you_have_no_chats">You have no chats</string>
|
<string name="you_have_no_chats">You have no chats</string>
|
||||||
<string name="loading_chats">Loading chats…</string>
|
<string name="loading_chats">Loading chats…</string>
|
||||||
<string name="no_filtered_chats">No filtered chats</string>
|
<string name="no_filtered_chats">No filtered chats</string>
|
||||||
|
<string name="no_contacts">No contacts</string>
|
||||||
|
<string name="no_filtered_contacts">No filtered contacts</string>
|
||||||
<string name="contact_tap_to_connect">Tap to Connect</string>
|
<string name="contact_tap_to_connect">Tap to Connect</string>
|
||||||
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
|
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
|
||||||
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
|
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
|
||||||
@@ -427,10 +430,28 @@
|
|||||||
<string name="notifications">Notifications</string>
|
<string name="notifications">Notifications</string>
|
||||||
|
|
||||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||||
|
<string name="info_view_connect_button">connect</string>
|
||||||
|
<string name="info_view_open_button">open</string>
|
||||||
|
<string name="info_view_message_button">message</string>
|
||||||
|
<string name="info_view_call_button">call</string>
|
||||||
|
<string name="info_view_video_button">video</string>
|
||||||
<string name="delete_contact_question">Delete contact?</string>
|
<string name="delete_contact_question">Delete contact?</string>
|
||||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string>
|
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string>
|
||||||
|
<string name="delete_contact_cannot_undo_warning">Contact will be deleted - this cannot be undone!</string>
|
||||||
|
<string name="delete_conversation_question">Delete conversation?</string>
|
||||||
|
<string name="delete_conversation_all_messages_deleted_cannot_undo_warning">Conversation and all messages will be deleted - this cannot be undone!</string>
|
||||||
|
<string name="delete_conversation">Delete conversation</string>
|
||||||
|
<string name="only_delete_conversation">Only delete conversation</string>
|
||||||
|
<string name="delete_contact_keep_conversation">Delete contact, keep conversation</string>
|
||||||
|
<string name="notify_delete_contact_question">Notify contact?</string>
|
||||||
|
<string name="confirm_delete_contact_question">Confirm contact deletion?</string>
|
||||||
<string name="delete_and_notify_contact">Delete and notify contact</string>
|
<string name="delete_and_notify_contact">Delete and notify contact</string>
|
||||||
|
<string name="delete_without_notification">Delete without notification</string>
|
||||||
<string name="button_delete_contact">Delete contact</string>
|
<string name="button_delete_contact">Delete contact</string>
|
||||||
|
<string name="conversation_deleted">Conversation deleted!</string>
|
||||||
|
<string name="you_can_still_send_messages_to_contact">You can still send messages to %1$s from the Contacts tab.</string>
|
||||||
|
<string name="contact_deleted">Contact deleted!</string>
|
||||||
|
<string name="you_can_still_view_conversation_with_contact">You can still view conversation with %1$s in the Chats tab.</string>
|
||||||
<string name="text_field_set_contact_placeholder">Set contact name…</string>
|
<string name="text_field_set_contact_placeholder">Set contact name…</string>
|
||||||
<string name="icon_descr_server_status_connected">Connected</string>
|
<string name="icon_descr_server_status_connected">Connected</string>
|
||||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||||
@@ -617,6 +638,7 @@
|
|||||||
<!-- NewChatView.kt -->
|
<!-- NewChatView.kt -->
|
||||||
<string name="new_chat">New chat</string>
|
<string name="new_chat">New chat</string>
|
||||||
<string name="add_contact_tab">Add contact</string>
|
<string name="add_contact_tab">Add contact</string>
|
||||||
|
<string name="scan_paste_link">Scan / Paste link</string>
|
||||||
<string name="one_time_link">One-time invitation link</string>
|
<string name="one_time_link">One-time invitation link</string>
|
||||||
<string name="one_time_link_short">1-time link</string>
|
<string name="one_time_link_short">1-time link</string>
|
||||||
<string name="simplex_address">SimpleX address</string>
|
<string name="simplex_address">SimpleX address</string>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M241.78-244.5 134-136.5q-13.5 13.5-31.25 6.52Q85-136.97 85-156.5V-818q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h675q22.97 0 40.23 17.27Q875-840.97 875-818v516q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27H241.78Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 337 B |
-74
@@ -1,74 +0,0 @@
|
|||||||
package chat.simplex.common.views.chatlist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.*
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import chat.simplex.common.platform.*
|
|
||||||
import chat.simplex.common.ui.theme.*
|
|
||||||
import chat.simplex.common.views.call.Call
|
|
||||||
import chat.simplex.common.views.call.CallMediaType
|
|
||||||
import chat.simplex.common.views.chat.item.ItemAction
|
|
||||||
import chat.simplex.common.views.helpers.*
|
|
||||||
import chat.simplex.res.MR
|
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
|
||||||
// if (call.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) {
|
|
||||||
if (!newChatSheetState.collectAsState().value.isVisible()) {
|
|
||||||
val showMenu = remember { mutableStateOf(false) }
|
|
||||||
val media = call.peerMedia ?: call.localMedia
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalIndication provides NoIndication
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.BottomEnd
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.padding(end = 71.dp, bottom = 92.dp)
|
|
||||||
.size(67.dp)
|
|
||||||
.combinedClickable(onClick = {
|
|
||||||
val chat = chatModel.getChat(call.contact.id)
|
|
||||||
if (chat != null) {
|
|
||||||
withBGApi {
|
|
||||||
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = { showMenu.value = true })
|
|
||||||
.onRightClick { showMenu.value = true },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) {
|
|
||||||
ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image)
|
|
||||||
}
|
|
||||||
Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) {
|
|
||||||
if (media == CallMediaType.Video) {
|
|
||||||
Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White)
|
|
||||||
} else {
|
|
||||||
Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DefaultDropdownMenu(showMenu) {
|
|
||||||
ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = {
|
|
||||||
withBGApi { chatModel.callManager.endCall(call) }
|
|
||||||
showMenu.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
package chat.simplex.common.views.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.call.Call
|
||||||
|
import chat.simplex.common.views.call.CallMediaType
|
||||||
|
import chat.simplex.common.views.chat.item.ItemAction
|
||||||
|
import chat.simplex.common.views.chatlist.NoIndication
|
||||||
|
import chat.simplex.common.views.chatlist.openChat
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
||||||
|
// if (call.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||||
|
if (!newChatSheetState.collectAsState().value.isVisible()) {
|
||||||
|
val showMenu = remember { mutableStateOf(false) }
|
||||||
|
val media = call.peerMedia ?: call.localMedia
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalIndication provides NoIndication
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomEnd
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.padding(end = 71.dp, bottom = 92.dp)
|
||||||
|
.size(67.dp)
|
||||||
|
.combinedClickable(onClick = {
|
||||||
|
val chat = chatModel.getChat(call.contact.id)
|
||||||
|
if (chat != null) {
|
||||||
|
withBGApi {
|
||||||
|
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { showMenu.value = true })
|
||||||
|
.onRightClick { showMenu.value = true },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) {
|
||||||
|
ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image)
|
||||||
|
}
|
||||||
|
Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) {
|
||||||
|
if (media == CallMediaType.Video) {
|
||||||
|
Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White)
|
||||||
|
} else {
|
||||||
|
Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DefaultDropdownMenu(showMenu) {
|
||||||
|
ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = {
|
||||||
|
withBGApi { chatModel.callManager.endCall(call) }
|
||||||
|
showMenu.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ defaultAppSettings =
|
|||||||
uiDarkColorScheme = Just DCSSimplex,
|
uiDarkColorScheme = Just DCSSimplex,
|
||||||
uiCurrentThemeIds = Nothing,
|
uiCurrentThemeIds = Nothing,
|
||||||
uiThemes = Nothing,
|
uiThemes = Nothing,
|
||||||
oneHandUI = Just True
|
oneHandUI = Just False
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultParseAppSettings :: AppSettings
|
defaultParseAppSettings :: AppSettings
|
||||||
|
|||||||
Reference in New Issue
Block a user