mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
ios: moved and rename major tag components to match android/desktop (#5459)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
parent
bbb58c8e09
commit
d81ae757eb
4 changed files with 418 additions and 404 deletions
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
import ElegantEmojiPicker
|
|
||||||
|
|
||||||
typealias DynamicSizes = (
|
typealias DynamicSizes = (
|
||||||
rowHeight: CGFloat,
|
rowHeight: CGFloat,
|
||||||
|
@ -343,9 +342,9 @@ struct ChatListNavLink: View {
|
||||||
AnyView(
|
AnyView(
|
||||||
NavigationView {
|
NavigationView {
|
||||||
if chatTagsModel.userTags.isEmpty {
|
if chatTagsModel.userTags.isEmpty {
|
||||||
ChatListTagEditor(chat: chat)
|
TagListEditor(chat: chat)
|
||||||
} else {
|
} else {
|
||||||
ChatListTag(chat: chat)
|
TagListView(chat: chat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -560,403 +559,6 @@ struct ChatListNavLink: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TagEditorNavParams {
|
|
||||||
let chat: Chat?
|
|
||||||
let chatListTag: ChatTagData?
|
|
||||||
let tagId: Int64?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChatListTag: View {
|
|
||||||
var chat: Chat? = nil
|
|
||||||
@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 {
|
|
||||||
setChatTag(tagId: selected ? nil : tagId, chat: c) { dismiss() }
|
|
||||||
} 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 chat == nil {
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> Void) {
|
|
||||||
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 {
|
|
||||||
let m = ChatModel.shared
|
|
||||||
let tm = ChatTagsModel.shared
|
|
||||||
tm.userTags = userTags
|
|
||||||
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
|
|
||||||
tm.decTagsReadCount(tags)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: false)
|
|
||||||
closeSheet()
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
showAlert(
|
|
||||||
NSLocalizedString("Error saving chat list", comment: "alert title"),
|
|
||||||
message: responseError(error)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
|
||||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
|
||||||
@EnvironmentObject var theme: AppTheme
|
|
||||||
var chat: Chat? = nil
|
|
||||||
var tagId: Int64? = nil
|
|
||||||
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(
|
|
||||||
chat != nil
|
|
||||||
? "Add to list"
|
|
||||||
: "Save list"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.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 text = trimmedName
|
|
||||||
let userTags = try await apiCreateChatTag(
|
|
||||||
tag: ChatTagData(emoji: newEmoji , text: text)
|
|
||||||
)
|
|
||||||
await MainActor.run {
|
|
||||||
saving = false
|
|
||||||
chatTagsModel.userTags = userTags
|
|
||||||
}
|
|
||||||
if let chat, let tag = userTags.first(where: { $0.chatTagText == text && $0.chatTagEmoji == newEmoji}) {
|
|
||||||
setChatTag(tagId: tag.chatTagId, chat: chat) { dismiss() }
|
|
||||||
} else {
|
|
||||||
await MainActor.run { 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 {
|
func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Reject contact request"),
|
title: Text("Reject contact request"),
|
||||||
|
|
|
@ -569,7 +569,7 @@ struct ChatListSearchBar: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
ScrollView([.horizontal], showsIndicators: false) { ChatTagsView(parentSheet: $parentSheet, searchText: $searchText) }
|
ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) }
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
|
@ -671,7 +671,7 @@ struct ChatListSearchBar: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatTagsView: View {
|
struct TagsView: View {
|
||||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@ -732,7 +732,7 @@ struct ChatTagsView: View {
|
||||||
content: {
|
content: {
|
||||||
AnyView(
|
AnyView(
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ChatListTag(chat: nil)
|
TagListView(chat: nil)
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -749,7 +749,7 @@ struct ChatTagsView: View {
|
||||||
content: {
|
content: {
|
||||||
AnyView(
|
AnyView(
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ChatListTagEditor()
|
TagListEditor()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
408
apps/ios/Shared/Views/ChatList/TagListView.swift
Normal file
408
apps/ios/Shared/Views/ChatList/TagListView.swift
Normal file
|
@ -0,0 +1,408 @@
|
||||||
|
//
|
||||||
|
// TagListView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Diogo Cunha on 31/12/2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
import ElegantEmojiPicker
|
||||||
|
|
||||||
|
struct TagEditorNavParams {
|
||||||
|
let chat: Chat?
|
||||||
|
let chatListTag: ChatTagData?
|
||||||
|
let tagId: Int64?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TagListView: View {
|
||||||
|
var chat: Chat? = nil
|
||||||
|
@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 {
|
||||||
|
setChatTag(tagId: selected ? nil : tagId, chat: c) { dismiss() }
|
||||||
|
} 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 {
|
||||||
|
TagListEditor(
|
||||||
|
chat: params.chat,
|
||||||
|
tagId: params.tagId,
|
||||||
|
emoji: params.chatListTag?.emoji,
|
||||||
|
name: params.chatListTag?.text ?? ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.opacity(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onMove(perform: moveItem)
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
TagListEditor(chat: chat)
|
||||||
|
} label: {
|
||||||
|
Label("Create list", systemImage: "plus")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
if chat == nil {
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> Void) {
|
||||||
|
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 {
|
||||||
|
let m = ChatModel.shared
|
||||||
|
let tm = ChatTagsModel.shared
|
||||||
|
tm.userTags = userTags
|
||||||
|
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
|
||||||
|
tm.decTagsReadCount(tags)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: false)
|
||||||
|
closeSheet()
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error saving chat list", comment: "alert title"),
|
||||||
|
message: responseError(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TagListEditor: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
var chat: Chat? = nil
|
||||||
|
var tagId: Int64? = nil
|
||||||
|
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(
|
||||||
|
chat != nil
|
||||||
|
? "Add to list"
|
||||||
|
: "Save list"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.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 text = trimmedName
|
||||||
|
let userTags = try await apiCreateChatTag(
|
||||||
|
tag: ChatTagData(emoji: newEmoji , text: text)
|
||||||
|
)
|
||||||
|
await MainActor.run {
|
||||||
|
saving = false
|
||||||
|
chatTagsModel.userTags = userTags
|
||||||
|
}
|
||||||
|
if let chat, let tag = userTags.first(where: { $0.chatTagText == text && $0.chatTagEmoji == newEmoji}) {
|
||||||
|
setChatTag(tagId: tag.chatTagId, chat: chat) { dismiss() }
|
||||||
|
} else {
|
||||||
|
await MainActor.run { 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -203,6 +203,7 @@
|
||||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
||||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||||
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
||||||
|
B70A39732D24090D00E80A5F /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70A39722D24090D00E80A5F /* TagListView.swift */; };
|
||||||
B728945B2D0C62BF00F7A19A /* ElegantEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = B728945A2D0C62BF00F7A19A /* ElegantEmojiPicker */; };
|
B728945B2D0C62BF00F7A19A /* ElegantEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = B728945A2D0C62BF00F7A19A /* ElegantEmojiPicker */; };
|
||||||
B73EFE532CE5FA3500C778EA /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */; };
|
B73EFE532CE5FA3500C778EA /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */; };
|
||||||
B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; };
|
B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; };
|
||||||
|
@ -552,6 +553,7 @@
|
||||||
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
|
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
|
||||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
||||||
|
B70A39722D24090D00E80A5F /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = "<group>"; };
|
||||||
B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; };
|
B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; };
|
||||||
B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
|
B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
|
||||||
B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCreationCard.swift; sourceTree = "<group>"; };
|
B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCreationCard.swift; sourceTree = "<group>"; };
|
||||||
|
@ -962,6 +964,7 @@
|
||||||
18415835CBD939A9ABDC108A /* UserPicker.swift */,
|
18415835CBD939A9ABDC108A /* UserPicker.swift */,
|
||||||
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */,
|
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */,
|
||||||
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */,
|
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */,
|
||||||
|
B70A39722D24090D00E80A5F /* TagListView.swift */,
|
||||||
);
|
);
|
||||||
path = ChatList;
|
path = ChatList;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1526,6 +1529,7 @@
|
||||||
8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */,
|
8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */,
|
||||||
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
|
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
|
||||||
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */,
|
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */,
|
||||||
|
B70A39732D24090D00E80A5F /* TagListView.swift in Sources */,
|
||||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
|
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
|
||||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
|
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
|
||||||
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue