mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 23:46:41 +00:00
core, ios: chat tags (#5367)
* types and db * migration module * chat tag * store method proposal * profiles build * update type * update return type * building * working api * update * refactor * attach tags to contact * simplify * attach chat tags to group info * get chat tags with supplied user id * get tags fix * ios: chat tags poc (#5370) * ios: chat tags poc * updates to sheet * temporary display for other option on swipe * sheet height * only show preset when it has matches * changes * worst emoji picker ever * simplify tag casts and collapse * open on create tag if no tags * simple emoji text field * nice emoji picker * dismiss sheets on tag/untag * semibold selection * all preset tag and change collapsed icon on selection * default selected tag (all) * only apply tag filters on empty search * + button when no custom lists * reset selection of tag filter on profile changes * edit tag (broken menu inside swiftui list) * create list to end of list * swipe changes * remove context menu * delete and edit on swipe actions * tap unread filter deselects other filters * remove delete tag if empty * show tag creation sheet when + button pressed * in memory tag edit * color, size * frame * layout * refactor * remove code * add unread to same unit * fraction on long press * nav fixes * in memory list * emoji picker improvements * remove diff * secondary plus * stop flickering on chat tags load * reuse string * fix reset glitches * delete destructive * simplify? * changes * api updates * fix styles on list via swipe * fixed untag * update schema * move user tags loading to get users chat data * move presets to model * update preset tags when chats are updated * style fixes and locate getPresetTags near tags model --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * deleted contacts and card should not match contact preset * fix update presets on chat remove * update migration indices * fix migration * not used chat model * disable button on repeated list name or emoji * no chats message for search fix * fix edits and trim * error in footer, not in alert * styling fixes due to wrong place to attach sheet * update library * remove log * idea for dynamic sheet height * max fraction 62% * minor fixes * disable save button when no changes and while saving * disable preset filter if it is no longer shown * remove comments from schema * fix emoji * remove apiChatTagsResponse * always read chat tags * fix --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
25 files changed
+1311
-116
No files matched your search
@@ -332,7 +332,7 @@ struct ChatInfoView: View {
|
||||
.sheet(item: $sheet) {
|
||||
if #available(iOS 16.0, *) {
|
||||
$0.content
|
||||
.presentationDetents([.fraction(0.4)])
|
||||
.presentationDetents([.fraction($0.fraction)])
|
||||
} else {
|
||||
$0.content
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import ElegantEmojiPicker
|
||||
|
||||
typealias DynamicSizes = (
|
||||
rowHeight: CGFloat,
|
||||
@@ -43,9 +44,11 @@ func dynamicSize(_ font: DynamicTypeSize) -> DynamicSizes {
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = false
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var parentSheet: SomeSheet<AnyView>?
|
||||
@State private var showContactRequestDialog = false
|
||||
@State private var showJoinGroupDialog = false
|
||||
@State private var showContactConnectionInfo = false
|
||||
@@ -85,6 +88,7 @@ struct ChatListNavLink: View {
|
||||
progressByTimeout = false
|
||||
}
|
||||
}
|
||||
.actionSheet(item: $actionSheet) { $0.actionSheet }
|
||||
}
|
||||
|
||||
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
|
||||
@@ -124,6 +128,7 @@ struct ChatListNavLink: View {
|
||||
toggleNtfsButton(chat: chat)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
tagChatButton(chat)
|
||||
if !chat.chatItems.isEmpty {
|
||||
clearChatButton()
|
||||
}
|
||||
@@ -145,11 +150,10 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { $0.alert }
|
||||
.actionSheet(item: $actionSheet) { $0.actionSheet }
|
||||
.sheet(item: $sheet) {
|
||||
if #available(iOS 16.0, *) {
|
||||
$0.content
|
||||
.presentationDetents([.fraction(0.4)])
|
||||
.presentationDetents([.fraction($0.fraction)])
|
||||
} else {
|
||||
$0.content
|
||||
}
|
||||
@@ -185,6 +189,7 @@ struct ChatListNavLink: View {
|
||||
AlertManager.shared.showAlert(groupInvitationAcceptedAlert())
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
tagChatButton(chat)
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
leaveGroupChatButton(groupInfo)
|
||||
}
|
||||
@@ -206,14 +211,25 @@ struct ChatListNavLink: View {
|
||||
toggleNtfsButton(chat: chat)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if !chat.chatItems.isEmpty {
|
||||
tagChatButton(chat)
|
||||
let showClearButton = !chat.chatItems.isEmpty
|
||||
let showDeleteGroup = groupInfo.canDelete
|
||||
let showLeaveGroup = groupInfo.membership.memberCurrent
|
||||
let totalNumberOfButtons = 1 + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0)
|
||||
|
||||
if showClearButton, totalNumberOfButtons <= 3 {
|
||||
clearChatButton()
|
||||
}
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
if (showLeaveGroup) {
|
||||
leaveGroupChatButton(groupInfo)
|
||||
}
|
||||
if groupInfo.canDelete {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
|
||||
if showDeleteGroup {
|
||||
if totalNumberOfButtons <= 3 {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
} else {
|
||||
moreOptionsButton(chat, groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,7 +322,67 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.tint(Color.orange)
|
||||
}
|
||||
|
||||
|
||||
private func tagChatButton(_ chat: Chat) -> some View {
|
||||
Button {
|
||||
setTagChatSheet(chat)
|
||||
} label: {
|
||||
SwipeLabel(NSLocalizedString("List", comment: "swipe action"), systemImage: "tag.fill", inverted: oneHandUI)
|
||||
}
|
||||
.tint(.mint)
|
||||
}
|
||||
|
||||
private func setTagChatSheet(_ chat: Chat) {
|
||||
let screenHeight = UIScreen.main.bounds.height
|
||||
let reservedSpace: Double = 4 * 44 // 2 for padding, 1 for "Create list" and another for extra tag
|
||||
let tagsSpace = Double(max(chatTagsModel.userTags.count, 3)) * 44
|
||||
let fraction = min((reservedSpace + tagsSpace) / screenHeight, 0.62)
|
||||
|
||||
parentSheet = SomeSheet(
|
||||
content: {
|
||||
AnyView(
|
||||
NavigationView {
|
||||
if chatTagsModel.userTags.isEmpty {
|
||||
ChatListTagEditor(chat: chat)
|
||||
} else {
|
||||
ChatListTag(chat: chat)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
id: "lists sheet",
|
||||
fraction: fraction
|
||||
)
|
||||
}
|
||||
|
||||
private func moreOptionsButton(_ chat: Chat, _ groupInfo: GroupInfo?) -> some View {
|
||||
Button {
|
||||
var buttons: [Alert.Button] = [
|
||||
.default(Text("Clear")) {
|
||||
AlertManager.shared.showAlert(clearChatAlert())
|
||||
}
|
||||
]
|
||||
|
||||
if let gi = groupInfo, gi.canDelete {
|
||||
buttons.append(.destructive(Text("Delete")) {
|
||||
AlertManager.shared.showAlert(deleteGroupAlert(gi))
|
||||
})
|
||||
}
|
||||
|
||||
buttons.append(.cancel())
|
||||
|
||||
actionSheet = SomeActionSheet(
|
||||
actionSheet: ActionSheet(
|
||||
title: Text("Clear or delete group?"),
|
||||
buttons: buttons
|
||||
),
|
||||
id: "other options"
|
||||
)
|
||||
} label: {
|
||||
SwipeLabel(NSLocalizedString("More", comment: "swipe action"), systemImage: "ellipsis", inverted: oneHandUI)
|
||||
}
|
||||
}
|
||||
|
||||
private func clearNoteFolderButton() -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlert(clearNoteFolderAlert())
|
||||
@@ -484,6 +560,389 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct TagEditorNavParams {
|
||||
let chat: Chat?
|
||||
let chatListTag: ChatTagData?
|
||||
let tagId: Int64?
|
||||
}
|
||||
|
||||
struct ChatListTag: View {
|
||||
var chat: Chat? = nil
|
||||
var showEditButton: Bool = false
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State private var editMode = EditMode.inactive
|
||||
@State private var tagEditorNavParams: TagEditorNavParams? = nil
|
||||
|
||||
var chatTagsIds: [Int64] { chat?.chatInfo.contact?.chatTags ?? chat?.chatInfo.groupInfo?.chatTags ?? [] }
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
ForEach(chatTagsModel.userTags, id: \.id) { tag in
|
||||
let text = tag.chatTagText
|
||||
let emoji = tag.chatTagEmoji
|
||||
let tagId = tag.chatTagId
|
||||
let selected = chatTagsIds.contains(tagId)
|
||||
|
||||
HStack {
|
||||
if let emoji {
|
||||
Text(emoji)
|
||||
} else {
|
||||
Image(systemName: "tag")
|
||||
}
|
||||
Text(text)
|
||||
.padding(.leading, 12)
|
||||
Spacer()
|
||||
if chat != nil {
|
||||
radioButton(selected: selected)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if let c = chat {
|
||||
setTag(tagId: selected ? nil : tagId, chat: c)
|
||||
} else {
|
||||
tagEditorNavParams = TagEditorNavParams(chat: nil, chatListTag: ChatTagData(emoji: emoji, text: text), tagId: tagId)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button {
|
||||
showAlert(
|
||||
NSLocalizedString("Delete list?", comment: "alert title"),
|
||||
message: NSLocalizedString("All chats will be removed from the list \(text), and the list deleted.", comment: "alert message"),
|
||||
actions: {[
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Cancel", comment: "alert action"),
|
||||
style: .default
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Delete", comment: "alert action"),
|
||||
style: .destructive,
|
||||
handler: { _ in
|
||||
deleteTag(tagId)
|
||||
}
|
||||
)
|
||||
]}
|
||||
)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash.fill")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
Button {
|
||||
tagEditorNavParams = TagEditorNavParams(chat: nil, chatListTag: ChatTagData(emoji: emoji, text: text), tagId: tagId)
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.background(
|
||||
// isActive required to navigate to edit view from any possible tag edited in swipe action
|
||||
NavigationLink(isActive: Binding(get: { tagEditorNavParams != nil }, set: { _ in tagEditorNavParams = nil })) {
|
||||
if let params = tagEditorNavParams {
|
||||
ChatListTagEditor(
|
||||
chat: params.chat,
|
||||
tagId: params.tagId,
|
||||
emoji: params.chatListTag?.emoji,
|
||||
name: params.chatListTag?.text ?? ""
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.opacity(0)
|
||||
)
|
||||
}
|
||||
.onMove(perform: moveItem)
|
||||
|
||||
NavigationLink {
|
||||
ChatListTagEditor(chat: chat)
|
||||
} label: {
|
||||
Label("Create list", systemImage: "plus")
|
||||
}
|
||||
} header: {
|
||||
if showEditButton {
|
||||
editTagsButton()
|
||||
.textCase(nil)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.environment(\.editMode, $editMode)
|
||||
}
|
||||
|
||||
private func editTagsButton() -> some View {
|
||||
if editMode.isEditing {
|
||||
Button("Done") {
|
||||
editMode = .inactive
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
Button("Edit") {
|
||||
editMode = .active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func radioButton(selected: Bool) -> some View {
|
||||
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel))
|
||||
}
|
||||
|
||||
private func moveItem(from source: IndexSet, to destination: Int) {
|
||||
Task {
|
||||
do {
|
||||
var tags = chatTagsModel.userTags
|
||||
tags.move(fromOffsets: source, toOffset: destination)
|
||||
try await apiReorderChatTags(tagIds: tags.map { $0.chatTagId })
|
||||
|
||||
await MainActor.run {
|
||||
chatTagsModel.userTags = tags
|
||||
}
|
||||
} catch let error {
|
||||
showAlert(
|
||||
NSLocalizedString("Error reordering lists", comment: "alert title"),
|
||||
message: responseError(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setTag(tagId: Int64?, chat: Chat) {
|
||||
Task {
|
||||
do {
|
||||
let tagIds: [Int64] = if let t = tagId { [t] } else {[]}
|
||||
let (userTags, chatTags) = try await apiSetChatTags(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
tagIds: tagIds
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
chatTagsModel.userTags = userTags
|
||||
if var contact = chat.chatInfo.contact {
|
||||
contact.chatTags = chatTags
|
||||
m.updateContact(contact)
|
||||
} else if var group = chat.chatInfo.groupInfo {
|
||||
group.chatTags = chatTags
|
||||
m.updateGroup(group)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
showAlert(
|
||||
NSLocalizedString("Error saving chat list", comment: "alert title"),
|
||||
message: responseError(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteTag(_ tagId: Int64) {
|
||||
Task {
|
||||
try await apiDeleteChatTag(tagId: tagId)
|
||||
|
||||
await MainActor.run {
|
||||
chatTagsModel.userTags = chatTagsModel.userTags.filter { $0.chatTagId != tagId }
|
||||
if case let .userTag(tag) = chatTagsModel.activeFilter, tagId == tag.chatTagId {
|
||||
chatTagsModel.activeFilter = nil
|
||||
}
|
||||
m.chats.forEach { c in
|
||||
if var contact = c.chatInfo.contact, contact.chatTags.contains(tagId) {
|
||||
contact.chatTags = contact.chatTags.filter({ $0 != tagId })
|
||||
m.updateContact(contact)
|
||||
} else if var group = c.chatInfo.groupInfo, group.chatTags.contains(tagId) {
|
||||
group.chatTags = group.chatTags.filter({ $0 != tagId })
|
||||
m.updateGroup(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EmojiPickerView: UIViewControllerRepresentable {
|
||||
@Binding var selectedEmoji: String?
|
||||
@Binding var showingPicker: Bool
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
class Coordinator: NSObject, ElegantEmojiPickerDelegate, UIAdaptivePresentationControllerDelegate {
|
||||
var parent: EmojiPickerView
|
||||
|
||||
init(parent: EmojiPickerView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func emojiPicker(_ picker: ElegantEmojiPicker, didSelectEmoji emoji: Emoji?) {
|
||||
parent.selectedEmoji = emoji?.emoji
|
||||
parent.showingPicker = false
|
||||
picker.dismiss(animated: true)
|
||||
}
|
||||
|
||||
// Called when the picker is dismissed manually (without selection)
|
||||
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
|
||||
parent.showingPicker = false
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(parent: self)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
let config = ElegantConfiguration(showRandom: false, showReset: true, showClose: false)
|
||||
let picker = ElegantEmojiPicker(delegate: context.coordinator, configuration: config)
|
||||
|
||||
picker.presentationController?.delegate = context.coordinator
|
||||
|
||||
let viewController = UIViewController()
|
||||
DispatchQueue.main.async {
|
||||
if let topVC = getTopViewController() {
|
||||
topVC.present(picker, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
return viewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
|
||||
// No need to update the controller after creation
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatListTagEditor: View {
|
||||
var chat: Chat? = nil
|
||||
var tagId: Int64? = nil
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var emoji: String?
|
||||
var name: String = ""
|
||||
@State private var newEmoji: String?
|
||||
@State private var newName: String = ""
|
||||
@State private var isPickerPresented = false
|
||||
@State private var saving: Bool?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
let isDuplicateEmojiOrName = chatTagsModel.userTags.contains { tag in
|
||||
tag.chatTagId != tagId &&
|
||||
((newEmoji != nil && tag.chatTagEmoji == newEmoji) || tag.chatTagText == trimmedName)
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Button {
|
||||
isPickerPresented = true
|
||||
} label: {
|
||||
if let newEmoji {
|
||||
Text(newEmoji)
|
||||
} else {
|
||||
Image(systemName: "face.smiling")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
TextField("List name...", text: $newName)
|
||||
}
|
||||
|
||||
Button {
|
||||
saving = true
|
||||
if let tId = tagId {
|
||||
updateChatTag(tagId: tId, chatTagData: ChatTagData(emoji: newEmoji, text: trimmedName))
|
||||
} else {
|
||||
createChatTag()
|
||||
}
|
||||
} label: {
|
||||
Text(NSLocalizedString(tagId == nil ? "Create list" : "Save list", comment: "list editor button"))
|
||||
}
|
||||
.disabled(saving != nil || (trimmedName == name && newEmoji == emoji) || trimmedName.isEmpty || isDuplicateEmojiOrName)
|
||||
} footer: {
|
||||
if isDuplicateEmojiOrName && saving != false { // if not saved already, to prevent flickering
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.foregroundColor(.red)
|
||||
Text("List name and emoji should be different for all lists.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isPickerPresented {
|
||||
EmojiPickerView(selectedEmoji: $newEmoji, showingPicker: $isPickerPresented)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.onAppear {
|
||||
newEmoji = emoji
|
||||
newName = name
|
||||
}
|
||||
}
|
||||
|
||||
var trimmedName: String {
|
||||
newName.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
private func createChatTag() {
|
||||
Task {
|
||||
do {
|
||||
let userTags = try await apiCreateChatTag(
|
||||
tag: ChatTagData(emoji: newEmoji , text: trimmedName)
|
||||
)
|
||||
await MainActor.run {
|
||||
saving = false
|
||||
chatTagsModel.userTags = userTags
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
saving = nil
|
||||
showAlert(
|
||||
NSLocalizedString("Error creating list", comment: "alert title"),
|
||||
message: responseError(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateChatTag(tagId: Int64, chatTagData: ChatTagData) {
|
||||
Task {
|
||||
do {
|
||||
try await apiUpdateChatTag(tagId: tagId, tag: chatTagData)
|
||||
await MainActor.run {
|
||||
saving = false
|
||||
for i in 0..<chatTagsModel.userTags.count {
|
||||
if chatTagsModel.userTags[i].chatTagId == tagId {
|
||||
chatTagsModel.userTags[i] = ChatTag(
|
||||
chatTagId: tagId,
|
||||
chatTagText: chatTagData.text,
|
||||
chatTagEmoji: chatTagData.emoji
|
||||
)
|
||||
}
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
saving = nil
|
||||
showAlert(
|
||||
NSLocalizedString("Error creating list", comment: "alert title"),
|
||||
message: responseError(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
|
||||
Alert(
|
||||
title: Text("Reject contact request"),
|
||||
@@ -585,15 +1044,15 @@ struct ChatListNavLink_Previews: PreviewProvider {
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), parentSheet: .constant(nil))
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), parentSheet: .constant(nil))
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.contactRequest,
|
||||
chatItems: []
|
||||
))
|
||||
), parentSheet: .constant(nil))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 82))
|
||||
}
|
||||
|
||||
@@ -31,6 +31,29 @@ enum UserPickerSheet: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
enum PresetTag: Int, Identifiable, CaseIterable, Equatable {
|
||||
case favorites = 0
|
||||
case contacts = 1
|
||||
case groups = 2
|
||||
case business = 3
|
||||
|
||||
var id: Int { rawValue }
|
||||
}
|
||||
|
||||
enum ActiveFilter: Identifiable, Equatable {
|
||||
case presetTag(PresetTag)
|
||||
case userTag(ChatTag)
|
||||
case unread
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .presetTag(tag): "preset \(tag.id)"
|
||||
case let .userTag(tag): "user \(tag.chatTagId)"
|
||||
case .unread: "unread"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SaveableSettings: ObservableObject {
|
||||
@Published var servers: ServerSettings = ServerSettings(currUserServers: [], userServers: [], serverErrors: [])
|
||||
}
|
||||
@@ -117,13 +140,14 @@ struct ChatListView: View {
|
||||
@State private var searchChatFilteredBySimplexLink: String? = nil
|
||||
@State private var scrollToSearchBar = false
|
||||
@State private var userPickerShown: Bool = false
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
@State private var sheet: SomeSheet<AnyView>? = nil
|
||||
@StateObject private var chatTagsModel = ChatTagsModel.shared
|
||||
|
||||
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
|
||||
@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false
|
||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
viewBody.scrollDismissesKeyboard(.immediately)
|
||||
@@ -131,7 +155,7 @@ struct ChatListView: View {
|
||||
viewBody
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var viewBody: some View {
|
||||
ZStack(alignment: oneHandUI ? .bottomLeading : .topLeading) {
|
||||
NavStackCompat(
|
||||
@@ -161,8 +185,9 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.environmentObject(chatTagsModel)
|
||||
}
|
||||
|
||||
|
||||
private var chatListView: some View {
|
||||
let tm = ToolbarMaterial.material(toolbarMaterial)
|
||||
return withToolbar(tm) {
|
||||
@@ -197,15 +222,22 @@ struct ChatListView: View {
|
||||
Divider().padding(.bottom, Self.hasHomeIndicator ? 0 : 8).background(tm)
|
||||
}
|
||||
}
|
||||
.sheet(item: $sheet) { sheet in
|
||||
if #available(iOS 16.0, *) {
|
||||
sheet.content.presentationDetents([.fraction(sheet.fraction)])
|
||||
} else {
|
||||
sheet.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static var hasHomeIndicator: Bool = {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first {
|
||||
window.safeAreaInsets.bottom > 0
|
||||
} else { false }
|
||||
}()
|
||||
|
||||
|
||||
@ViewBuilder func withToolbar(_ material: Material, content: () -> some View) -> some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
if oneHandUI {
|
||||
@@ -226,13 +258,13 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ToolbarContentBuilder var topToolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .topBarLeading) { leadingToolbarItem }
|
||||
ToolbarItem(placement: .principal) { SubsStatusIndicator() }
|
||||
ToolbarItem(placement: .topBarTrailing) { trailingToolbarItem }
|
||||
}
|
||||
|
||||
|
||||
@ToolbarContentBuilder var bottomToolbar: some ToolbarContent {
|
||||
let padding: Double = Self.hasHomeIndicator ? 0 : 14
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
@@ -247,7 +279,7 @@ struct ChatListView: View {
|
||||
.onTapGesture { scrollToSearchBar = true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ToolbarContentBuilder var bottomToolbarGroup: some ToolbarContent {
|
||||
let padding: Double = Self.hasHomeIndicator ? 0 : 14
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
@@ -258,7 +290,7 @@ struct ChatListView: View {
|
||||
trailingToolbarItem.padding(.bottom, padding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder var leadingToolbarItem: some View {
|
||||
let user = chatModel.currentUser ?? User.sampleData
|
||||
ZStack(alignment: .topTrailing) {
|
||||
@@ -275,7 +307,7 @@ struct ChatListView: View {
|
||||
userPickerShown = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder var trailingToolbarItem: some View {
|
||||
switch chatModel.chatRunning {
|
||||
case .some(true): NewChatMenuButton()
|
||||
@@ -283,7 +315,7 @@ struct ChatListView: View {
|
||||
case .none: EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder private var chatList: some View {
|
||||
let cs = filteredChats()
|
||||
ZStack {
|
||||
@@ -295,7 +327,8 @@ struct ChatListView: View {
|
||||
searchFocussed: $searchFocussed,
|
||||
searchText: $searchText,
|
||||
searchShowingSimplexLink: $searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
|
||||
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink,
|
||||
parentSheet: $sheet
|
||||
)
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.listRowSeparator(.hidden)
|
||||
@@ -306,7 +339,7 @@ struct ChatListView: View {
|
||||
}
|
||||
if #available(iOS 16.0, *) {
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
ChatListNavLink(chat: chat, parentSheet: $sheet)
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
@@ -318,7 +351,7 @@ struct ChatListView: View {
|
||||
VStack(spacing: .zero) {
|
||||
Divider()
|
||||
.padding(.leading, 16)
|
||||
ChatListNavLink(chat: chat)
|
||||
ChatListNavLink(chat: chat, parentSheet: $sheet)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
@@ -363,80 +396,97 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
if cs.isEmpty && !chatModel.chats.isEmpty {
|
||||
Text("No filtered chats")
|
||||
noChatsView()
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func noChatsView() -> some View {
|
||||
if searchString().isEmpty {
|
||||
switch chatTagsModel.activeFilter {
|
||||
case .presetTag: Text("No filtered chats") // this should not happen
|
||||
case let .userTag(tag): Text("No chats in list \(tag.chatTagText)")
|
||||
case .unread:
|
||||
Button {
|
||||
chatTagsModel.activeFilter = nil
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "line.3.horizontal.decrease")
|
||||
Text("No unread chats")
|
||||
}
|
||||
}
|
||||
case .none: Text("No chats")
|
||||
}
|
||||
} else {
|
||||
Text("No chats found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func unreadBadge(size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
ChatView(chat: chat)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func stopAudioPlayer() {
|
||||
VoiceItemState.smallView.values.forEach { $0.audioPlayer?.stop() }
|
||||
VoiceItemState.smallView = [:]
|
||||
}
|
||||
|
||||
|
||||
private func filteredChats() -> [Chat] {
|
||||
if let linkChatId = searchChatFilteredBySimplexLink {
|
||||
return chatModel.chats.filter { $0.id == linkChatId }
|
||||
} else {
|
||||
let s = searchString()
|
||||
return s == "" && !showUnreadAndFavorites
|
||||
return s == ""
|
||||
? chatModel.chats.filter { chat in
|
||||
!chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card
|
||||
!chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat)
|
||||
}
|
||||
: chatModel.chats.filter { chat in
|
||||
let cInfo = chat.chatInfo
|
||||
switch cInfo {
|
||||
return switch cInfo {
|
||||
case let .direct(contact):
|
||||
return !contact.chatDeleted && chatContactType(chat: chat) != ContactType.card && (
|
||||
s == ""
|
||||
? filtered(chat)
|
||||
: (viewNameContains(cInfo, s) ||
|
||||
contact.profile.displayName.localizedLowercase.contains(s) ||
|
||||
contact.fullName.localizedLowercase.contains(s))
|
||||
!contact.chatDeleted && !chat.chatInfo.contactCard && (
|
||||
( viewNameContains(cInfo, s) ||
|
||||
contact.profile.displayName.localizedLowercase.contains(s) ||
|
||||
contact.fullName.localizedLowercase.contains(s)
|
||||
)
|
||||
)
|
||||
case let .group(gInfo):
|
||||
return s == ""
|
||||
? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
|
||||
: viewNameContains(cInfo, s)
|
||||
case .local:
|
||||
return s == "" || viewNameContains(cInfo, s)
|
||||
case .contactRequest:
|
||||
return s == "" || viewNameContains(cInfo, s)
|
||||
case let .contactConnection(conn):
|
||||
return s != "" && conn.localAlias.localizedLowercase.contains(s)
|
||||
case .invalidJSON:
|
||||
return false
|
||||
case .group: viewNameContains(cInfo, s)
|
||||
case .local: viewNameContains(cInfo, s)
|
||||
case .contactRequest: viewNameContains(cInfo, s)
|
||||
case let .contactConnection(conn): conn.localAlias.localizedLowercase.contains(s)
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchString() -> String {
|
||||
searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
}
|
||||
|
||||
|
||||
func filtered(_ chat: Chat) -> Bool {
|
||||
(chat.chatInfo.chatSettings?.favorite ?? false) ||
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
switch chatTagsModel.activeFilter {
|
||||
case let .presetTag(tag): presetTagMatchesChat(tag, chat)
|
||||
case let .userTag(tag): chat.chatInfo.chatTags?.contains(tag.chatTagId) == true
|
||||
case .unread: chat.chatStats.unreadChat || chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0
|
||||
case .none: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
||||
cInfo.chatViewName.localizedLowercase.contains(s)
|
||||
}
|
||||
}
|
||||
|
||||
func searchString() -> String {
|
||||
searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
}
|
||||
}
|
||||
|
||||
struct SubsStatusIndicator: View {
|
||||
@@ -500,18 +550,20 @@ struct SubsStatusIndicator: View {
|
||||
struct ChatListSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||
@Binding var searchMode: Bool
|
||||
@FocusState.Binding var searchFocussed: Bool
|
||||
@Binding var searchText: String
|
||||
@Binding var searchShowingSimplexLink: Bool
|
||||
@Binding var searchChatFilteredBySimplexLink: String?
|
||||
@Binding var parentSheet: SomeSheet<AnyView>?
|
||||
@State private var ignoreSearchTextChange = false
|
||||
@State private var alert: PlanAndConnectAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
ScrollView([.horizontal], showsIndicators: false) { ChatTagsView(parentSheet: $parentSheet) }
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
@@ -578,16 +630,21 @@ struct ChatListSearchBar: View {
|
||||
}
|
||||
|
||||
private func toggleFilterButton() -> some View {
|
||||
ZStack {
|
||||
let showUnread = chatTagsModel.activeFilter == .unread
|
||||
return ZStack {
|
||||
Color.clear
|
||||
.frame(width: 22, height: 22)
|
||||
Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
|
||||
Image(systemName: showUnread ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(showUnreadAndFavorites ? theme.colors.primary : theme.colors.secondary)
|
||||
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
|
||||
.foregroundColor(showUnread ? theme.colors.primary : theme.colors.secondary)
|
||||
.frame(width: showUnread ? 22 : 16, height: showUnread ? 22 : 16)
|
||||
.onTapGesture {
|
||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||
if chatTagsModel.activeFilter == .unread {
|
||||
chatTagsModel.activeFilter = nil
|
||||
} else {
|
||||
chatTagsModel.activeFilter = .unread
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -605,6 +662,179 @@ struct ChatListSearchBar: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatTagsView: View {
|
||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var parentSheet: SomeSheet<AnyView>?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
tagsView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func tagsView() -> some View {
|
||||
if chatTagsModel.presetTags.count > 1 {
|
||||
if chatTagsModel.presetTags.count + chatTagsModel.userTags.count <= 3 {
|
||||
expandedPresetTagsFiltersView()
|
||||
} else {
|
||||
collapsedTagsFilterView()
|
||||
}
|
||||
}
|
||||
ForEach(chatTagsModel.userTags, id: \.id) { tag in
|
||||
let current = if case let .userTag(t) = chatTagsModel.activeFilter {
|
||||
t == tag
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
let color: Color = current ? .accentColor : .secondary
|
||||
ZStack {
|
||||
HStack(spacing: 4) {
|
||||
if let emoji = tag.chatTagEmoji {
|
||||
Text(emoji)
|
||||
} else {
|
||||
Image(systemName: current ? "tag.fill" : "tag")
|
||||
.foregroundColor(color)
|
||||
}
|
||||
ZStack {
|
||||
Text(tag.chatTagText).fontWeight(.semibold).foregroundColor(.clear)
|
||||
Text(tag.chatTagText).fontWeight(current ? .semibold : .regular).foregroundColor(color)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
setActiveFilter(filter: .userTag(tag))
|
||||
}
|
||||
.onLongPressGesture {
|
||||
let screenHeight = UIScreen.main.bounds.height
|
||||
let reservedSpace: Double = 4 * 44 // 2 for padding, 1 for "Create list" and another for extra tag
|
||||
let tagsSpace = Double(max(chatTagsModel.userTags.count, 3)) * 44
|
||||
let fraction = min((reservedSpace + tagsSpace) / screenHeight, 0.62)
|
||||
|
||||
parentSheet = SomeSheet(
|
||||
content: {
|
||||
AnyView(
|
||||
NavigationView {
|
||||
ChatListTag(chat: nil, showEditButton: true)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
)
|
||||
},
|
||||
id: "tag list",
|
||||
fraction: fraction
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
parentSheet = SomeSheet(
|
||||
content: {
|
||||
AnyView(
|
||||
NavigationView {
|
||||
ChatListTagEditor()
|
||||
}
|
||||
)
|
||||
},
|
||||
id: "tag create"
|
||||
)
|
||||
} label: {
|
||||
if chatTagsModel.userTags.isEmpty {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "plus")
|
||||
Text("Add list")
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
@ViewBuilder private func expandedPresetTagsFiltersView() -> some View {
|
||||
let selectedPresetTag: PresetTag? = if case let .presetTag(tag) = chatTagsModel.activeFilter {
|
||||
tag
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
ForEach(chatTagsModel.presetTags, id: \.id) { tag in
|
||||
let active = tag == selectedPresetTag
|
||||
let (icon, text) = presetTagLabel(tag: tag, active: active)
|
||||
let color: Color = active ? .accentColor : .secondary
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(color)
|
||||
ZStack {
|
||||
Text(text).fontWeight(.semibold).foregroundColor(.clear)
|
||||
Text(text).fontWeight(active ? .semibold : .regular).foregroundColor(color)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
setActiveFilter(filter: .presetTag(tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func collapsedTagsFilterView() -> some View {
|
||||
let selectedPresetTag: PresetTag? = if case let .presetTag(tag) = chatTagsModel.activeFilter {
|
||||
tag
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
Menu {
|
||||
if selectedPresetTag != nil {
|
||||
Button {
|
||||
chatTagsModel.activeFilter = nil
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "list.bullet")
|
||||
Text("All")
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(chatTagsModel.presetTags, id: \.id) { tag in
|
||||
Button {
|
||||
setActiveFilter(filter: .presetTag(tag))
|
||||
} label: {
|
||||
let (systemName, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag)
|
||||
HStack {
|
||||
Image(systemName: systemName)
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if let tag = selectedPresetTag {
|
||||
let (systemName, _) = presetTagLabel(tag: tag, active: true)
|
||||
Image(systemName: systemName)
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Image(systemName: "list.bullet")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 28)
|
||||
}
|
||||
|
||||
private func presetTagLabel(tag: PresetTag, active: Bool) -> (String, LocalizedStringKey) {
|
||||
switch tag {
|
||||
case .favorites: (active ? "star.fill" : "star", "Favorites")
|
||||
case .contacts: (active ? "person.fill" : "person", "Contacts")
|
||||
case .groups: (active ? "person.2.fill" : "person.2", "Groups")
|
||||
case .business: (active ? "briefcase.fill" : "briefcase", "Businesses")
|
||||
}
|
||||
}
|
||||
|
||||
private func setActiveFilter(filter: ActiveFilter) {
|
||||
if filter != chatTagsModel.activeFilter {
|
||||
chatTagsModel.activeFilter = filter
|
||||
} else {
|
||||
chatTagsModel.activeFilter = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func chatStoppedIcon() -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -616,6 +846,28 @@ func chatStoppedIcon() -> some View {
|
||||
}
|
||||
}
|
||||
|
||||
func presetTagMatchesChat(_ tag: PresetTag, _ chat: Chat) -> Bool {
|
||||
switch tag {
|
||||
case .favorites:
|
||||
chat.chatInfo.chatSettings?.favorite == true
|
||||
case .contacts:
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact): !(contact.activeConn == nil && contact.profile.contactLink != nil && contact.active) && !contact.chatDeleted
|
||||
case .contactRequest: true
|
||||
case .contactConnection: true
|
||||
case let .group(groupInfo): groupInfo.businessChat?.chatType == .customer
|
||||
default: false
|
||||
}
|
||||
case .groups:
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo): groupInfo.businessChat == nil
|
||||
default: false
|
||||
}
|
||||
case .business:
|
||||
chat.chatInfo.groupInfo?.businessChat?.chatType == .business
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatListView_Previews: PreviewProvider {
|
||||
@State static var userPickerSheet: UserPickerSheet? = .none
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ struct ContactListNavLink: View {
|
||||
@State private var showContactRequestDialog = false
|
||||
|
||||
var body: some View {
|
||||
let contactType = chatContactType(chat: chat)
|
||||
let contactType = chatContactType(chat)
|
||||
|
||||
Group {
|
||||
switch (chat.chatInfo) {
|
||||
|
||||
@@ -186,7 +186,7 @@ struct NewChatSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
func chatContactType(chat: Chat) -> ContactType {
|
||||
func chatContactType(_ chat: Chat) -> ContactType {
|
||||
switch chat.chatInfo {
|
||||
case .contactRequest:
|
||||
return .request
|
||||
@@ -207,7 +207,7 @@ func chatContactType(chat: Chat) -> ContactType {
|
||||
|
||||
private func filterContactTypes(chats: [Chat], contactTypes: [ContactType]) -> [Chat] {
|
||||
return chats.filter { chat in
|
||||
contactTypes.contains(chatContactType(chat: chat))
|
||||
contactTypes.contains(chatContactType(chat))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,8 +279,8 @@ struct ContactsList: View {
|
||||
}
|
||||
|
||||
private func chatsByTypeComparator(chat1: Chat, chat2: Chat) -> Bool {
|
||||
let chat1Type = chatContactType(chat: chat1)
|
||||
let chat2Type = chatContactType(chat: chat2)
|
||||
let chat1Type = chatContactType(chat1)
|
||||
let chat2Type = chatContactType(chat2)
|
||||
|
||||
if chat1Type.rawValue < chat2Type.rawValue {
|
||||
return true
|
||||
|
||||
@@ -25,6 +25,7 @@ struct SomeActionSheet: Identifiable {
|
||||
struct SomeSheet<Content: View>: Identifiable {
|
||||
@ViewBuilder var content: Content
|
||||
var id: String
|
||||
var fraction = 0.4
|
||||
}
|
||||
|
||||
private enum NewChatViewAlert: Identifiable {
|
||||
|
||||
@@ -21,7 +21,7 @@ struct AddressCreationCard: View {
|
||||
var body: some View {
|
||||
let addressExists = chatModel.userAddress != nil
|
||||
let chats = chatModel.chats.filter { chat in
|
||||
!chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card
|
||||
!chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard
|
||||
}
|
||||
ZStack(alignment: .topTrailing) {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
|
||||
Reference in new issue
Block a user