mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
ios: ChatView performance improvements (#4353)
* feat: Add synthesized hashable conformance to chat and API types (#4348) * UIKit ReverseList * ReverseList - manual layout updates for external state * Propagate ScrollModel; Disable async media width * Filter chat items * Remove UIKit menu wrapper * Make chat item width calculation synchronous (#4371) * Fix floating button regression * Improve filter performance * Fix page load the merged items exceed full page * Resolve iOS15 compabibility * Restore build config * Add page-up scroll; Fix same item decrementing unread counter multiple times * Fix: Chat not loading additional pages, if newest items are all merged and exceed page size * Minor * Fix item loading regression * Fix item loading regression 2 * Fix unread regression * refactor --------- Co-authored-by: Arturs Krumins <auth@levitatingpineapple.com> Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e22e01acd2
commit
2585f4ecfd
@@ -332,12 +332,12 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
withConditionalAnimation {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
withAnimation(itemAnimation()) {
|
||||
withConditionalAnimation(itemAnimation()) {
|
||||
var ci = cItem
|
||||
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
||||
ci.meta.itemStatus = status
|
||||
@@ -357,7 +357,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
withConditionalAnimation {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
}
|
||||
} else if let status = status {
|
||||
@@ -512,11 +512,13 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
// update preview
|
||||
decreaseUnreadCounter(cInfo)
|
||||
// update current chat
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
markChatItemRead_(i)
|
||||
if reversedChatItems[i].isRcvNew {
|
||||
// update current chat
|
||||
markChatItemRead_(i)
|
||||
// update preview
|
||||
decreaseUnreadCounter(cInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,7 +725,7 @@ struct NTFContactRequest {
|
||||
var chatId: String
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts {
|
||||
struct UnreadChatItemCounts: Equatable {
|
||||
var totalBelow: Int
|
||||
var unreadBelow: Int
|
||||
}
|
||||
|
||||
@@ -309,8 +309,10 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] {
|
||||
throw r
|
||||
}
|
||||
|
||||
let loadItemsPerPage = 50
|
||||
|
||||
func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat {
|
||||
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search))
|
||||
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search))
|
||||
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -13,10 +13,9 @@ struct CIImageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let chatItem: ChatItem
|
||||
let image: String
|
||||
var preview: UIImage?
|
||||
let maxWidth: CGFloat
|
||||
@Binding var imgWidth: CGFloat?
|
||||
@State var scrollProxy: ScrollViewProxy?
|
||||
var imgWidth: CGFloat?
|
||||
@State private var showFullScreenImage = false
|
||||
|
||||
var body: some View {
|
||||
@@ -25,15 +24,14 @@ struct CIImageView: View {
|
||||
if let uiImage = getLoadedImage(file) {
|
||||
imageView(uiImage)
|
||||
.fullScreenCover(isPresented: $showFullScreenImage) {
|
||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
|
||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage)
|
||||
}
|
||||
.onTapGesture { showFullScreenImage = true }
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenImage = false
|
||||
}
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
imageView(uiImage)
|
||||
} else if let preview {
|
||||
imageView(preview)
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
@@ -90,7 +88,6 @@ struct CIImageView: View {
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { imgWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
if img.imageData == nil {
|
||||
Image(uiImage: img)
|
||||
|
||||
@@ -15,14 +15,12 @@ struct CIVideoView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
private let chatItem: ChatItem
|
||||
private let image: String
|
||||
private let preview: UIImage?
|
||||
@State private var duration: Int
|
||||
@State private var progress: Int = 0
|
||||
@State private var videoPlaying: Bool = false
|
||||
private let maxWidth: CGFloat
|
||||
@Binding private var videoWidth: CGFloat?
|
||||
@State private var scrollProxy: ScrollViewProxy?
|
||||
@State private var preview: UIImage? = nil
|
||||
private var videoWidth: CGFloat?
|
||||
@State private var player: AVPlayer?
|
||||
@State private var fullPlayer: AVPlayer?
|
||||
@State private var url: URL?
|
||||
@@ -33,13 +31,12 @@ struct CIVideoView: View {
|
||||
@State private var fullScreenTimeObserver: Any? = nil
|
||||
@State private var publisher: AnyCancellable? = nil
|
||||
|
||||
init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding<CGFloat?>, scrollProxy: ScrollViewProxy?) {
|
||||
init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) {
|
||||
self.chatItem = chatItem
|
||||
self.image = image
|
||||
self.preview = preview
|
||||
self._duration = State(initialValue: duration)
|
||||
self.maxWidth = maxWidth
|
||||
self._videoWidth = videoWidth
|
||||
self.scrollProxy = scrollProxy
|
||||
self.videoWidth = videoWidth
|
||||
if let url = getLoadedVideo(chatItem.file) {
|
||||
let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet()
|
||||
self._urlDecrypted = State(initialValue: decrypted)
|
||||
@@ -49,10 +46,6 @@ struct CIVideoView: View {
|
||||
}
|
||||
self._url = State(initialValue: url)
|
||||
}
|
||||
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
self._preview = State(initialValue: uiImage)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -63,9 +56,8 @@ struct CIVideoView: View {
|
||||
videoView(player, decrypted, file, preview, duration)
|
||||
} else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil {
|
||||
videoViewEncrypted(file, defaultPreview, duration)
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
imageView(uiImage)
|
||||
} else if let preview {
|
||||
imageView(preview)
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
@@ -152,7 +144,6 @@ struct CIVideoView: View {
|
||||
|
||||
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
|
||||
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { videoWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
ZStack(alignment: .center) {
|
||||
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
|
||||
@@ -252,7 +243,6 @@ struct CIVideoView: View {
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { videoWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
|
||||
@@ -18,15 +18,16 @@ private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, o
|
||||
|
||||
struct FramedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var preview: UIImage?
|
||||
@Binding var revealed: Bool
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@State var msgWidth: CGFloat = 0
|
||||
@State var imgWidth: CGFloat? = nil
|
||||
@State var videoWidth: CGFloat? = nil
|
||||
var imgWidth: CGFloat? = nil
|
||||
var videoWidth: CGFloat? = nil
|
||||
@State var metaColor = Color.secondary
|
||||
@State var showFullScreenImage = false
|
||||
@Binding var allowMenu: Bool
|
||||
@@ -58,10 +59,9 @@ struct FramedItemView: View {
|
||||
if let qi = chatItem.quotedItem {
|
||||
ciQuoteView(qi)
|
||||
.onTapGesture {
|
||||
if let proxy = scrollProxy,
|
||||
let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
||||
if let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
||||
withAnimation {
|
||||
proxy.scrollTo(ci.viewId, anchor: .bottom)
|
||||
scrollModel.scrollToItem(id: ci.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,7 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
@@ -114,8 +115,8 @@ struct FramedItemView: View {
|
||||
.padding(.bottom, 2)
|
||||
} else {
|
||||
switch (chatItem.content.msgContent) {
|
||||
case let .image(text, image):
|
||||
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
|
||||
case let .image(text, _):
|
||||
CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth)
|
||||
.overlay(DetermineWidth())
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
@@ -127,8 +128,8 @@ struct FramedItemView: View {
|
||||
} else {
|
||||
ciMsgContentView(chatItem)
|
||||
}
|
||||
case let .video(text, image, duration):
|
||||
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
|
||||
case let .video(text, _, duration):
|
||||
CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth)
|
||||
.overlay(DetermineWidth())
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
@@ -181,7 +182,6 @@ struct FramedItemView: View {
|
||||
.padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0)
|
||||
.overlay(DetermineWidth())
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, colorScheme))
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
|
||||
@@ -13,12 +13,12 @@ import AVKit
|
||||
|
||||
struct FullScreenMediaView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
|
||||
@State var chatItem: ChatItem
|
||||
@State var image: UIImage?
|
||||
@State var player: AVPlayer? = nil
|
||||
@State var url: URL? = nil
|
||||
@Binding var showView: Bool
|
||||
@State var scrollProxy: ScrollViewProxy?
|
||||
@State private var showNext = false
|
||||
@State private var nextImage: UIImage?
|
||||
@State private var nextPlayer: AVPlayer?
|
||||
@@ -71,9 +71,7 @@ struct FullScreenMediaView: View {
|
||||
let w = abs(t.width)
|
||||
if t.height > 60 && t.height > w * 2 {
|
||||
showView = false
|
||||
if let proxy = scrollProxy {
|
||||
proxy.scrollTo(chatItem.viewId)
|
||||
}
|
||||
scrollModel.scrollToItem(id: chatItem.id)
|
||||
} else if w > 60 && w > abs(t.height) * 2 && !scrolling {
|
||||
let previous = t.width > 0
|
||||
scrolling = true
|
||||
|
||||
@@ -13,7 +13,6 @@ struct ChatItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@Binding var revealed: Bool
|
||||
@Binding var allowMenu: Bool
|
||||
@Binding var audioPlayer: AudioPlayer?
|
||||
@@ -24,7 +23,6 @@ struct ChatItemView: View {
|
||||
chatItem: ChatItem,
|
||||
showMember: Bool = false,
|
||||
maxWidth: CGFloat = .infinity,
|
||||
scrollProxy: ScrollViewProxy? = nil,
|
||||
revealed: Binding<Bool>,
|
||||
allowMenu: Binding<Bool> = .constant(false),
|
||||
audioPlayer: Binding<AudioPlayer?> = .constant(nil),
|
||||
@@ -34,7 +32,6 @@ struct ChatItemView: View {
|
||||
self.chat = chat
|
||||
self.chatItem = chatItem
|
||||
self.maxWidth = maxWidth
|
||||
_scrollProxy = .init(initialValue: scrollProxy)
|
||||
_revealed = revealed
|
||||
_allowMenu = allowMenu
|
||||
_audioPlayer = audioPlayer
|
||||
@@ -62,7 +59,37 @@ struct ChatItemView: View {
|
||||
}
|
||||
|
||||
private func framedItemView() -> some View {
|
||||
FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
let preview = chatItem.content.msgContent
|
||||
.flatMap {
|
||||
switch $0 {
|
||||
case let .image(_, image): image
|
||||
case let .video(_, image, _): image
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
.map { dropImagePrefix($0) }
|
||||
.flatMap { Data(base64Encoded: $0) }
|
||||
.flatMap { UIImage(data: $0) }
|
||||
let adjustedMaxWidth = {
|
||||
if let preview, preview.size.width <= preview.size.height {
|
||||
maxWidth * 0.75
|
||||
} else {
|
||||
maxWidth
|
||||
}
|
||||
}()
|
||||
return FramedItemView(
|
||||
chat: chat,
|
||||
chatItem: chatItem,
|
||||
preview: preview,
|
||||
revealed: $revealed,
|
||||
maxWidth: maxWidth,
|
||||
imgWidth: adjustedMaxWidth,
|
||||
videoWidth: adjustedMaxWidth,
|
||||
allowMenu: $allowMenu,
|
||||
audioPlayer: $audioPlayer,
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
//
|
||||
// ReverseList.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Levitating Pineapple on 11/06/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// A List, which displays it's items in reverse order - from bottom to top
|
||||
struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIViewControllerRepresentable {
|
||||
|
||||
let items: Array<Item>
|
||||
|
||||
@Binding var scrollState: ReverseListScrollModel<Item>.State
|
||||
|
||||
/// Closure, that returns user interface for a given item
|
||||
let content: (Item) -> Content
|
||||
|
||||
let loadPage: () -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> Controller {
|
||||
Controller(representer: self)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ controller: Controller, context: Context) {
|
||||
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
|
||||
switch destination {
|
||||
case .nextPage:
|
||||
controller.scrollToNextPage()
|
||||
case let .item(id):
|
||||
controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom)
|
||||
case .bottom:
|
||||
controller.scroll(to: .zero, position: .top)
|
||||
}
|
||||
} else {
|
||||
controller.update(items: items)
|
||||
}
|
||||
}
|
||||
|
||||
/// Controller, which hosts SwiftUI cells
|
||||
class Controller: UITableViewController {
|
||||
private enum Section { case main }
|
||||
private let representer: ReverseList
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
private var itemCount: Int = .zero
|
||||
private var bag = Set<AnyCancellable>()
|
||||
|
||||
init(representer: ReverseList) {
|
||||
self.representer = representer
|
||||
super.init(style: .plain)
|
||||
|
||||
// 1. Style
|
||||
tableView.separatorStyle = .none
|
||||
tableView.transform = .verticalFlip
|
||||
|
||||
// 2. Register cells
|
||||
if #available(iOS 16.0, *) {
|
||||
tableView.register(
|
||||
UITableViewCell.self,
|
||||
forCellReuseIdentifier: cellReuseId
|
||||
)
|
||||
} else {
|
||||
tableView.register(
|
||||
HostingCell<Content>.self,
|
||||
forCellReuseIdentifier: cellReuseId
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Configure data source
|
||||
self.dataSource = UITableViewDiffableDataSource<Section, Item>(
|
||||
tableView: tableView
|
||||
) { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
|
||||
self.representer.loadPage()
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
|
||||
if #available(iOS 16.0, *) {
|
||||
cell.contentConfiguration = UIHostingConfiguration { self.representer.content(item) }
|
||||
.margins(.all, .zero)
|
||||
.minSize(height: 1) // Passing zero will result in system default of 44 points being used
|
||||
} else {
|
||||
if let cell = cell as? HostingCell<Content> {
|
||||
cell.set(content: self.representer.content(item), parent: self)
|
||||
} else {
|
||||
fatalError("Unexpected Cell Type for: \(item)")
|
||||
}
|
||||
}
|
||||
cell.transform = .verticalFlip
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
|
||||
// 4. External state changes will require manual layout updates
|
||||
NotificationCenter.default
|
||||
.addObserver(
|
||||
self,
|
||||
selector: #selector(updateLayout),
|
||||
name: notificationName,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
deinit { NotificationCenter.default.removeObserver(self) }
|
||||
|
||||
@objc private func updateLayout() {
|
||||
if #available(iOS 16.0, *) {
|
||||
tableView.setNeedsLayout()
|
||||
tableView.layoutIfNeeded()
|
||||
} else {
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides keyboard, when user begins to scroll.
|
||||
/// Equivalent to `.scrollDismissesKeyboard(.immediately)`
|
||||
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
UIApplication.shared
|
||||
.sendAction(
|
||||
#selector(UIResponder.resignFirstResponder),
|
||||
to: nil,
|
||||
from: nil,
|
||||
for: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Scrolls up
|
||||
func scrollToNextPage() {
|
||||
tableView.setContentOffset(
|
||||
CGPoint(
|
||||
x: tableView.contentOffset.x,
|
||||
y: tableView.contentOffset.y + tableView.bounds.height
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
Task { representer.scrollState = .atDestination }
|
||||
}
|
||||
|
||||
/// Scrolls to Item at index path
|
||||
/// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil`
|
||||
func scroll(to index: Int?, position: UITableView.ScrollPosition) {
|
||||
if let index {
|
||||
var animated = false
|
||||
if #available(iOS 16.0, *) {
|
||||
animated = true
|
||||
}
|
||||
tableView.scrollToRow(
|
||||
at: IndexPath(row: index, section: .zero),
|
||||
at: position,
|
||||
animated: animated
|
||||
)
|
||||
Task { representer.scrollState = .atDestination }
|
||||
}
|
||||
}
|
||||
|
||||
func update(items: Array<Item>) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items)
|
||||
dataSource.defaultRowAnimation = .none
|
||||
dataSource.apply(
|
||||
snapshot,
|
||||
animatingDifferences: itemCount != .zero && abs(items.count - itemCount) == 1
|
||||
)
|
||||
itemCount = items.count
|
||||
}
|
||||
}
|
||||
|
||||
/// `UIHostingConfiguration` back-port for iOS14 and iOS15
|
||||
/// Implemented as a `UITableViewCell` that wraps and manages a generic `UIHostingController`
|
||||
private final class HostingCell<Hosted: View>: UITableViewCell {
|
||||
private let hostingController = UIHostingController<Hosted?>(rootView: nil)
|
||||
|
||||
/// Updates content of the cell
|
||||
/// For reference: https://noahgilmore.com/blog/swiftui-self-sizing-cells/
|
||||
func set(content: Hosted, parent: UIViewController) {
|
||||
hostingController.rootView = content
|
||||
if let hostingView = hostingController.view {
|
||||
hostingView.invalidateIntrinsicContentSize()
|
||||
if hostingController.parent != parent { parent.addChild(hostingController) }
|
||||
if !contentView.subviews.contains(hostingController.view) {
|
||||
contentView.addSubview(hostingController.view)
|
||||
hostingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
hostingView.leadingAnchor
|
||||
.constraint(equalTo: contentView.leadingAnchor),
|
||||
hostingView.trailingAnchor
|
||||
.constraint(equalTo: contentView.trailingAnchor),
|
||||
hostingView.topAnchor
|
||||
.constraint(equalTo: contentView.topAnchor),
|
||||
hostingView.bottomAnchor
|
||||
.constraint(equalTo: contentView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
if hostingController.parent != parent { hostingController.didMove(toParent: parent) }
|
||||
} else {
|
||||
fatalError("Hosting View not loaded \(hostingController)")
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
hostingController.rootView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages ``ReverseList`` scrolling
|
||||
class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
|
||||
/// Represents Scroll State of ``ReverseList``
|
||||
enum State: Equatable {
|
||||
enum Destination: Equatable {
|
||||
case nextPage
|
||||
case item(Item.ID)
|
||||
case bottom
|
||||
}
|
||||
|
||||
case scrollingTo(Destination)
|
||||
case atDestination
|
||||
}
|
||||
|
||||
@Published var state: State = .atDestination
|
||||
|
||||
func scrollToNextPage() {
|
||||
state = .scrollingTo(.nextPage)
|
||||
}
|
||||
|
||||
func scrollToBottom() {
|
||||
state = .scrollingTo(.bottom)
|
||||
}
|
||||
|
||||
func scrollToItem(id: Item.ID) {
|
||||
state = .scrollingTo(.item(id))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let cellReuseId = "hostingCell"
|
||||
|
||||
fileprivate let notificationName = NSNotification.Name(rawValue: "reverseListNeedsLayout")
|
||||
|
||||
fileprivate extension CGAffineTransform {
|
||||
/// Transform that vertically flips the view, preserving it's location
|
||||
static let verticalFlip = CGAffineTransform(scaleX: 1, y: -1)
|
||||
}
|
||||
|
||||
extension NotificationCenter {
|
||||
static func postReverseListNeedsLayout() {
|
||||
NotificationCenter.default.post(
|
||||
name: notificationName,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable animation on iOS 15
|
||||
func withConditionalAnimation<Result>(
|
||||
_ animation: Animation? = .default,
|
||||
_ body: () throws -> Result
|
||||
) rethrows -> Result {
|
||||
if #available(iOS 16.0, *) {
|
||||
try withAnimation(animation, body)
|
||||
} else {
|
||||
try body()
|
||||
}
|
||||
}
|
||||
@@ -220,9 +220,7 @@ struct ChatListView: View {
|
||||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
ChatView(chat: chat).onAppear {
|
||||
loadChat(chat: chat)
|
||||
}
|
||||
ChatView(chat: chat)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
//
|
||||
// ContextMenu2.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 09/08/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
|
||||
Group {
|
||||
if allowMenu.wrappedValue {
|
||||
if hasImageOrVideo {
|
||||
InteractionView(content:
|
||||
self.environmentObject(ChatModel.shared)
|
||||
.overlay(DetermineWidthImageVideoItem())
|
||||
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
|
||||
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
|
||||
.frame(maxWidth: itemWidth.wrappedValue)
|
||||
} else {
|
||||
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HostingViewHolder: UIView {
|
||||
var contentSize: CGSize = CGSizeMake(0, 0)
|
||||
override var intrinsicContentSize: CGSize { get { contentSize } }
|
||||
}
|
||||
|
||||
struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
let content: Content
|
||||
var maxWidth: CGFloat
|
||||
var itemWidth: Binding<CGFloat>
|
||||
@Binding var menu: UIMenu
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = HostingViewHolder()
|
||||
view.backgroundColor = .clear
|
||||
let hostView = UIHostingController(rootView: content)
|
||||
view.contentSize = hostView.view.intrinsicContentSize
|
||||
hostView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
let constraints = [
|
||||
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||
hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor)
|
||||
]
|
||||
view.addSubview(hostView.view)
|
||||
view.addConstraints(constraints)
|
||||
view.layer.cornerRadius = 18
|
||||
hostView.view.layer.cornerRadius = 18
|
||||
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
|
||||
view.addInteraction(menuInteraction)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
let was = (uiView as! HostingViewHolder).contentSize
|
||||
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity))
|
||||
if was != (uiView as! HostingViewHolder).contentSize {
|
||||
uiView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
|
||||
let parent: InteractionView<Content>
|
||||
|
||||
init(_ parent: InteractionView<Content>) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
configurationForMenuAtLocation location: CGPoint
|
||||
) -> UIContextMenuConfiguration? {
|
||||
UIContextMenuConfiguration(
|
||||
identifier: nil,
|
||||
previewProvider: nil,
|
||||
actionProvider: { [weak self] _ in
|
||||
guard let self = self else { return nil }
|
||||
return self.parent.menu
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// func contextMenuInteraction(
|
||||
// _ interaction: UIContextMenuInteraction,
|
||||
// willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
|
||||
// animator: UIContextMenuInteractionCommitAnimating
|
||||
// ) {
|
||||
// animator.addCompletion {
|
||||
// print("user tapped")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */; };
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
|
||||
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; };
|
||||
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; };
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
|
||||
@@ -186,6 +185,7 @@
|
||||
8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; };
|
||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||
CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */; };
|
||||
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
|
||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
|
||||
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
|
||||
@@ -267,7 +267,6 @@
|
||||
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = "<group>"; };
|
||||
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
|
||||
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
|
||||
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; };
|
||||
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
|
||||
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
|
||||
@@ -483,6 +482,7 @@
|
||||
8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDevicePicker.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>"; };
|
||||
CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.swift; sourceTree = "<group>"; };
|
||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
|
||||
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -591,6 +591,7 @@
|
||||
5CE4407127ADB1D0007B033A /* Emoji.swift */,
|
||||
5CADE79B292131E900072E13 /* ContactPreferencesView.swift */,
|
||||
5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */,
|
||||
CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */,
|
||||
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */,
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
|
||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */,
|
||||
@@ -651,7 +652,6 @@
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */,
|
||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */,
|
||||
5C6BA666289BD954009B8ECC /* DismissSheets.swift */,
|
||||
5C00164328A26FBC0094D739 /* ContextMenu.swift */,
|
||||
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */,
|
||||
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */,
|
||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */,
|
||||
@@ -1134,6 +1134,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */,
|
||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */,
|
||||
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */,
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */,
|
||||
@@ -1176,7 +1177,6 @@
|
||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
|
||||
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */,
|
||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */,
|
||||
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */,
|
||||
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */,
|
||||
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
|
||||
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */,
|
||||
|
||||
@@ -1010,20 +1010,20 @@ public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConnectionPlan: Decodable {
|
||||
public enum ConnectionPlan: Decodable, Hashable {
|
||||
case invitationLink(invitationLinkPlan: InvitationLinkPlan)
|
||||
case contactAddress(contactAddressPlan: ContactAddressPlan)
|
||||
case groupLink(groupLinkPlan: GroupLinkPlan)
|
||||
}
|
||||
|
||||
public enum InvitationLinkPlan: Decodable {
|
||||
public enum InvitationLinkPlan: Decodable, Hashable {
|
||||
case ok
|
||||
case ownLink
|
||||
case connecting(contact_: Contact?)
|
||||
case known(contact: Contact)
|
||||
}
|
||||
|
||||
public enum ContactAddressPlan: Decodable {
|
||||
public enum ContactAddressPlan: Decodable, Hashable {
|
||||
case ok
|
||||
case ownLink
|
||||
case connectingConfirmReconnect
|
||||
@@ -1032,7 +1032,7 @@ public enum ContactAddressPlan: Decodable {
|
||||
case contactViaAddress(contact: Contact)
|
||||
}
|
||||
|
||||
public enum GroupLinkPlan: Decodable {
|
||||
public enum GroupLinkPlan: Decodable, Hashable {
|
||||
case ok
|
||||
case ownLink(groupInfo: GroupInfo)
|
||||
case connectingConfirmReconnect
|
||||
@@ -1040,13 +1040,13 @@ public enum GroupLinkPlan: Decodable {
|
||||
case known(groupInfo: GroupInfo)
|
||||
}
|
||||
|
||||
struct NewUser: Encodable {
|
||||
struct NewUser: Encodable, Hashable {
|
||||
var profile: Profile?
|
||||
var sameServers: Bool
|
||||
var pastTimestamp: Bool
|
||||
}
|
||||
|
||||
public enum ChatPagination {
|
||||
public enum ChatPagination: Hashable {
|
||||
case last(count: Int)
|
||||
case after(chatItemId: Int64, count: Int)
|
||||
case before(chatItemId: Int64, count: Int)
|
||||
@@ -1268,7 +1268,7 @@ public struct ServerAddress: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct NetCfg: Codable, Equatable {
|
||||
public struct NetCfg: Codable, Equatable, Hashable {
|
||||
public var socksProxy: String? = nil
|
||||
var socksMode: SocksMode = .always
|
||||
public var hostMode: HostMode = .publicHost
|
||||
@@ -1314,18 +1314,18 @@ public struct NetCfg: Codable, Equatable {
|
||||
public var enableKeepAlive: Bool { tcpKeepAlive != nil }
|
||||
}
|
||||
|
||||
public enum HostMode: String, Codable {
|
||||
public enum HostMode: String, Codable, Hashable {
|
||||
case onionViaSocks
|
||||
case onionHost = "onion"
|
||||
case publicHost = "public"
|
||||
}
|
||||
|
||||
public enum SocksMode: String, Codable {
|
||||
public enum SocksMode: String, Codable, Hashable {
|
||||
case always = "always"
|
||||
case onion = "onion"
|
||||
}
|
||||
|
||||
public enum SMPProxyMode: String, Codable {
|
||||
public enum SMPProxyMode: String, Codable, Hashable {
|
||||
case always = "always"
|
||||
case unknown = "unknown"
|
||||
case unprotected = "unprotected"
|
||||
@@ -1345,7 +1345,7 @@ public enum SMPProxyMode: String, Codable {
|
||||
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
|
||||
}
|
||||
|
||||
public enum SMPProxyFallback: String, Codable {
|
||||
public enum SMPProxyFallback: String, Codable, Hashable {
|
||||
case allow = "allow"
|
||||
case allowProtected = "allowProtected"
|
||||
case prohibit = "prohibit"
|
||||
@@ -1363,7 +1363,7 @@ public enum SMPProxyFallback: String, Codable {
|
||||
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
|
||||
}
|
||||
|
||||
public enum OnionHosts: String, Identifiable {
|
||||
public enum OnionHosts: String, Identifiable, Hashable {
|
||||
case no
|
||||
case prefer
|
||||
case require
|
||||
@@ -1397,7 +1397,7 @@ public enum OnionHosts: String, Identifiable {
|
||||
public static let values: [OnionHosts] = [.no, .prefer, .require]
|
||||
}
|
||||
|
||||
public enum TransportSessionMode: String, Codable, Identifiable {
|
||||
public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
|
||||
case user
|
||||
case entity
|
||||
|
||||
@@ -1413,7 +1413,7 @@ public enum TransportSessionMode: String, Codable, Identifiable {
|
||||
public static let values: [TransportSessionMode] = [.user, .entity]
|
||||
}
|
||||
|
||||
public struct KeepAliveOpts: Codable, Equatable {
|
||||
public struct KeepAliveOpts: Codable, Equatable, Hashable {
|
||||
public var keepIdle: Int // seconds
|
||||
public var keepIntvl: Int // seconds
|
||||
public var keepCnt: Int // times
|
||||
@@ -1421,7 +1421,7 @@ public struct KeepAliveOpts: Codable, Equatable {
|
||||
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
|
||||
}
|
||||
|
||||
public enum NetworkStatus: Decodable, Equatable {
|
||||
public enum NetworkStatus: Decodable, Equatable, Hashable {
|
||||
case unknown
|
||||
case connected
|
||||
case disconnected
|
||||
@@ -1459,12 +1459,12 @@ public enum NetworkStatus: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConnNetworkStatus: Decodable {
|
||||
public struct ConnNetworkStatus: Decodable, Hashable {
|
||||
public var agentConnId: String
|
||||
public var networkStatus: NetworkStatus
|
||||
}
|
||||
|
||||
public struct ChatSettings: Codable {
|
||||
public struct ChatSettings: Codable, Hashable {
|
||||
public var enableNtfs: MsgFilter
|
||||
public var sendRcpts: Bool?
|
||||
public var favorite: Bool
|
||||
@@ -1478,13 +1478,13 @@ public struct ChatSettings: Codable {
|
||||
public static let defaults: ChatSettings = ChatSettings(enableNtfs: .all, sendRcpts: nil, favorite: false)
|
||||
}
|
||||
|
||||
public enum MsgFilter: String, Codable {
|
||||
public enum MsgFilter: String, Codable, Hashable {
|
||||
case none
|
||||
case all
|
||||
case mentions
|
||||
}
|
||||
|
||||
public struct UserMsgReceiptSettings: Codable {
|
||||
public struct UserMsgReceiptSettings: Codable, Hashable {
|
||||
public var enable: Bool
|
||||
public var clearOverrides: Bool
|
||||
|
||||
@@ -1494,7 +1494,7 @@ public struct UserMsgReceiptSettings: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConnectionStats: Decodable {
|
||||
public struct ConnectionStats: Decodable, Hashable {
|
||||
public var connAgentVersion: Int
|
||||
public var rcvQueuesInfo: [RcvQueueInfo]
|
||||
public var sndQueuesInfo: [SndQueueInfo]
|
||||
@@ -1510,30 +1510,30 @@ public struct ConnectionStats: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RcvQueueInfo: Codable {
|
||||
public struct RcvQueueInfo: Codable, Hashable {
|
||||
public var rcvServer: String
|
||||
public var rcvSwitchStatus: RcvSwitchStatus?
|
||||
public var canAbortSwitch: Bool
|
||||
}
|
||||
|
||||
public enum RcvSwitchStatus: String, Codable {
|
||||
public enum RcvSwitchStatus: String, Codable, Hashable {
|
||||
case switchStarted = "switch_started"
|
||||
case sendingQADD = "sending_qadd"
|
||||
case sendingQUSE = "sending_quse"
|
||||
case receivedMessage = "received_message"
|
||||
}
|
||||
|
||||
public struct SndQueueInfo: Codable {
|
||||
public struct SndQueueInfo: Codable, Hashable {
|
||||
public var sndServer: String
|
||||
public var sndSwitchStatus: SndSwitchStatus?
|
||||
}
|
||||
|
||||
public enum SndSwitchStatus: String, Codable {
|
||||
public enum SndSwitchStatus: String, Codable, Hashable {
|
||||
case sendingQKEY = "sending_qkey"
|
||||
case sendingQTEST = "sending_qtest"
|
||||
}
|
||||
|
||||
public enum QueueDirection: String, Decodable {
|
||||
public enum QueueDirection: String, Decodable, Hashable {
|
||||
case rcv
|
||||
case snd
|
||||
}
|
||||
@@ -1557,7 +1557,7 @@ public enum RatchetSyncState: String, Decodable {
|
||||
case agreed
|
||||
}
|
||||
|
||||
public struct UserContactLink: Decodable {
|
||||
public struct UserContactLink: Decodable, Hashable {
|
||||
public var connReqContact: String
|
||||
public var autoAccept: AutoAccept?
|
||||
|
||||
@@ -1571,7 +1571,7 @@ public struct UserContactLink: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AutoAccept: Codable {
|
||||
public struct AutoAccept: Codable, Hashable {
|
||||
public var acceptIncognito: Bool
|
||||
public var autoReply: MsgContent?
|
||||
|
||||
@@ -1593,7 +1593,7 @@ public protocol SelectableItem: Hashable, Identifiable {
|
||||
static var values: [Self] { get }
|
||||
}
|
||||
|
||||
public struct DeviceToken: Decodable {
|
||||
public struct DeviceToken: Decodable, Hashable {
|
||||
var pushProvider: PushProvider
|
||||
var token: String
|
||||
|
||||
@@ -1607,12 +1607,12 @@ public struct DeviceToken: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum PushEnvironment: String {
|
||||
public enum PushEnvironment: String, Hashable {
|
||||
case development
|
||||
case production
|
||||
}
|
||||
|
||||
public enum PushProvider: String, Decodable {
|
||||
public enum PushProvider: String, Decodable, Hashable {
|
||||
case apns_dev
|
||||
case apns_prod
|
||||
|
||||
@@ -1626,7 +1626,7 @@ public enum PushProvider: String, Decodable {
|
||||
|
||||
// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable,
|
||||
// and .local for periodic background checks
|
||||
public enum NotificationsMode: String, Decodable, SelectableItem {
|
||||
public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
|
||||
case off = "OFF"
|
||||
case periodic = "PERIODIC"
|
||||
case instant = "INSTANT"
|
||||
@@ -1644,7 +1644,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem {
|
||||
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
|
||||
}
|
||||
|
||||
public enum NotificationPreviewMode: String, SelectableItem, Codable {
|
||||
public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
|
||||
case hidden
|
||||
case contact
|
||||
case message
|
||||
@@ -1662,7 +1662,7 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable {
|
||||
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
|
||||
}
|
||||
|
||||
public struct RemoteCtrlInfo: Decodable {
|
||||
public struct RemoteCtrlInfo: Decodable, Hashable {
|
||||
public var remoteCtrlId: Int64
|
||||
public var ctrlDeviceName: String
|
||||
public var sessionState: RemoteCtrlSessionState?
|
||||
@@ -1672,7 +1672,7 @@ public struct RemoteCtrlInfo: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RemoteCtrlSessionState: Decodable {
|
||||
public enum RemoteCtrlSessionState: Decodable, Hashable {
|
||||
case starting
|
||||
case searching
|
||||
case connecting
|
||||
@@ -1687,17 +1687,17 @@ public enum RemoteCtrlStopReason: Decodable {
|
||||
case disconnected
|
||||
}
|
||||
|
||||
public struct CtrlAppInfo: Decodable {
|
||||
public struct CtrlAppInfo: Decodable, Hashable {
|
||||
public var appVersionRange: AppVersionRange
|
||||
public var deviceName: String
|
||||
}
|
||||
|
||||
public struct AppVersionRange: Decodable {
|
||||
public struct AppVersionRange: Decodable, Hashable {
|
||||
public var minVersion: String
|
||||
public var maxVersion: String
|
||||
}
|
||||
|
||||
public struct CoreVersionInfo: Decodable {
|
||||
public struct CoreVersionInfo: Decodable, Hashable {
|
||||
public var version: String
|
||||
public var simplexmqVersion: String
|
||||
public var simplexmqCommit: String
|
||||
@@ -1719,7 +1719,7 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
|
||||
encodeJSON(value).cString(using: .utf8)!
|
||||
}
|
||||
|
||||
public enum ChatError: Decodable {
|
||||
public enum ChatError: Decodable, Hashable {
|
||||
case error(errorType: ChatErrorType)
|
||||
case errorAgent(agentError: AgentErrorType)
|
||||
case errorStore(storeError: StoreError)
|
||||
@@ -1728,7 +1728,7 @@ public enum ChatError: Decodable {
|
||||
case invalidJSON(json: String)
|
||||
}
|
||||
|
||||
public enum ChatErrorType: Decodable {
|
||||
public enum ChatErrorType: Decodable, Hashable {
|
||||
case noActiveUser
|
||||
case noConnectionUser(agentConnId: String)
|
||||
case noSndFileUser(agentSndFileId: String)
|
||||
@@ -1807,7 +1807,7 @@ public enum ChatErrorType: Decodable {
|
||||
case exception(message: String)
|
||||
}
|
||||
|
||||
public enum StoreError: Decodable {
|
||||
public enum StoreError: Decodable, Hashable {
|
||||
case duplicateName
|
||||
case userNotFound(userId: Int64)
|
||||
case userNotFoundByName(contactName: ContactName)
|
||||
@@ -1867,7 +1867,7 @@ public enum StoreError: Decodable {
|
||||
case noGroupSndStatus(itemId: Int64, groupMemberId: Int64)
|
||||
}
|
||||
|
||||
public enum DatabaseError: Decodable {
|
||||
public enum DatabaseError: Decodable, Hashable {
|
||||
case errorEncrypted
|
||||
case errorPlaintext
|
||||
case errorNoFile(dbFile: String)
|
||||
@@ -1875,12 +1875,12 @@ public enum DatabaseError: Decodable {
|
||||
case errorOpen(sqliteError: SQLiteError)
|
||||
}
|
||||
|
||||
public enum SQLiteError: Decodable {
|
||||
public enum SQLiteError: Decodable, Hashable {
|
||||
case errorNotADatabase
|
||||
case error(String)
|
||||
}
|
||||
|
||||
public enum AgentErrorType: Decodable {
|
||||
public enum AgentErrorType: Decodable, Hashable {
|
||||
case CMD(cmdErr: CommandErrorType)
|
||||
case CONN(connErr: ConnectionErrorType)
|
||||
case SMP(smpErr: ProtocolErrorType)
|
||||
@@ -1894,7 +1894,7 @@ public enum AgentErrorType: Decodable {
|
||||
case INACTIVE
|
||||
}
|
||||
|
||||
public enum CommandErrorType: Decodable {
|
||||
public enum CommandErrorType: Decodable, Hashable {
|
||||
case PROHIBITED
|
||||
case SYNTAX
|
||||
case NO_CONN
|
||||
@@ -1902,7 +1902,7 @@ public enum CommandErrorType: Decodable {
|
||||
case LARGE
|
||||
}
|
||||
|
||||
public enum ConnectionErrorType: Decodable {
|
||||
public enum ConnectionErrorType: Decodable, Hashable {
|
||||
case NOT_FOUND
|
||||
case DUPLICATE
|
||||
case SIMPLEX
|
||||
@@ -1910,7 +1910,7 @@ public enum ConnectionErrorType: Decodable {
|
||||
case NOT_AVAILABLE
|
||||
}
|
||||
|
||||
public enum BrokerErrorType: Decodable {
|
||||
public enum BrokerErrorType: Decodable, Hashable {
|
||||
case RESPONSE(smpErr: String)
|
||||
case UNEXPECTED
|
||||
case NETWORK
|
||||
@@ -1919,7 +1919,7 @@ public enum BrokerErrorType: Decodable {
|
||||
case TIMEOUT
|
||||
}
|
||||
|
||||
public enum ProtocolErrorType: Decodable {
|
||||
public enum ProtocolErrorType: Decodable, Hashable {
|
||||
case BLOCK
|
||||
case SESSION
|
||||
case CMD(cmdErr: ProtocolCommandError)
|
||||
@@ -1930,7 +1930,7 @@ public enum ProtocolErrorType: Decodable {
|
||||
case INTERNAL
|
||||
}
|
||||
|
||||
public enum XFTPErrorType: Decodable {
|
||||
public enum XFTPErrorType: Decodable, Hashable {
|
||||
case BLOCK
|
||||
case SESSION
|
||||
case CMD(cmdErr: ProtocolCommandError)
|
||||
@@ -1947,7 +1947,7 @@ public enum XFTPErrorType: Decodable {
|
||||
case INTERNAL
|
||||
}
|
||||
|
||||
public enum RCErrorType: Decodable {
|
||||
public enum RCErrorType: Decodable, Hashable {
|
||||
case `internal`(internalErr: String)
|
||||
case identity
|
||||
case noLocalAddress
|
||||
@@ -1965,7 +1965,7 @@ public enum RCErrorType: Decodable {
|
||||
case syntax(syntaxErr: String)
|
||||
}
|
||||
|
||||
public enum ProtocolCommandError: Decodable {
|
||||
public enum ProtocolCommandError: Decodable, Hashable {
|
||||
case UNKNOWN
|
||||
case SYNTAX
|
||||
case PROHIBITED
|
||||
@@ -1974,7 +1974,7 @@ public enum ProtocolCommandError: Decodable {
|
||||
case NO_ENTITY
|
||||
}
|
||||
|
||||
public enum ProtocolTransportError: Decodable {
|
||||
public enum ProtocolTransportError: Decodable, Hashable {
|
||||
case badBlock
|
||||
case largeMsg
|
||||
case badSession
|
||||
@@ -1982,14 +1982,14 @@ public enum ProtocolTransportError: Decodable {
|
||||
case handshake(handshakeErr: SMPHandshakeError)
|
||||
}
|
||||
|
||||
public enum SMPHandshakeError: Decodable {
|
||||
public enum SMPHandshakeError: Decodable, Hashable {
|
||||
case PARSE
|
||||
case VERSION
|
||||
case IDENTITY
|
||||
case BAD_AUTH
|
||||
}
|
||||
|
||||
public enum SMPAgentError: Decodable {
|
||||
public enum SMPAgentError: Decodable, Hashable {
|
||||
case A_MESSAGE
|
||||
case A_PROHIBITED
|
||||
case A_VERSION
|
||||
@@ -1998,12 +1998,12 @@ public enum SMPAgentError: Decodable {
|
||||
case A_QUEUE(queueErr: String)
|
||||
}
|
||||
|
||||
public enum ArchiveError: Decodable {
|
||||
public enum ArchiveError: Decodable, Hashable {
|
||||
case `import`(chatError: ChatError)
|
||||
case importFile(file: String, chatError: ChatError)
|
||||
}
|
||||
|
||||
public enum RemoteCtrlError: Decodable {
|
||||
public enum RemoteCtrlError: Decodable, Hashable {
|
||||
case inactive
|
||||
case badState
|
||||
case busy
|
||||
@@ -2017,14 +2017,14 @@ public enum RemoteCtrlError: Decodable {
|
||||
case protocolError
|
||||
}
|
||||
|
||||
public struct MigrationFileLinkData: Codable {
|
||||
public struct MigrationFileLinkData: Codable, Hashable {
|
||||
let networkConfig: NetworkConfig?
|
||||
|
||||
public init(networkConfig: NetworkConfig) {
|
||||
self.networkConfig = networkConfig
|
||||
}
|
||||
|
||||
public struct NetworkConfig: Codable {
|
||||
public struct NetworkConfig: Codable, Hashable {
|
||||
let socksProxy: String?
|
||||
let hostMode: HostMode?
|
||||
let requiredHostMode: Bool?
|
||||
@@ -2056,7 +2056,7 @@ public struct MigrationFileLinkData: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AppSettings: Codable, Equatable {
|
||||
public struct AppSettings: Codable, Equatable, Hashable {
|
||||
public var networkConfig: NetCfg? = nil
|
||||
public var privacyEncryptLocalFiles: Bool? = nil
|
||||
public var privacyAskToApproveRelays: Bool? = nil
|
||||
@@ -2130,7 +2130,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum AppSettingsNotificationMode: String, Codable {
|
||||
public enum AppSettingsNotificationMode: String, Codable, Hashable {
|
||||
case off
|
||||
case periodic
|
||||
case instant
|
||||
@@ -2158,13 +2158,13 @@ public enum AppSettingsNotificationMode: String, Codable {
|
||||
// case message
|
||||
//}
|
||||
|
||||
public enum AppSettingsLockScreenCalls: String, Codable {
|
||||
public enum AppSettingsLockScreenCalls: String, Codable, Hashable {
|
||||
case disable
|
||||
case show
|
||||
case accept
|
||||
}
|
||||
|
||||
public struct UserNetworkInfo: Codable, Equatable {
|
||||
public struct UserNetworkInfo: Codable, Equatable, Hashable {
|
||||
public let networkType: UserNetworkType
|
||||
public let online: Bool
|
||||
|
||||
@@ -2174,7 +2174,7 @@ public struct UserNetworkInfo: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum UserNetworkType: String, Codable {
|
||||
public enum UserNetworkType: String, Codable, Hashable {
|
||||
case none
|
||||
case cellular
|
||||
case wifi
|
||||
@@ -2192,7 +2192,7 @@ public enum UserNetworkType: String, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RcvMsgInfo: Codable {
|
||||
public struct RcvMsgInfo: Codable, Hashable {
|
||||
var msgId: Int64
|
||||
var msgDeliveryId: Int64
|
||||
var msgDeliveryStatus: String
|
||||
@@ -2200,7 +2200,7 @@ public struct RcvMsgInfo: Codable {
|
||||
var agentMsgMeta: String
|
||||
}
|
||||
|
||||
public struct QueueInfo: Codable {
|
||||
public struct QueueInfo: Codable, Hashable {
|
||||
var qiSnd: Bool
|
||||
var qiNtf: Bool
|
||||
var qiSub: QSub?
|
||||
@@ -2208,25 +2208,25 @@ public struct QueueInfo: Codable {
|
||||
var qiMsg: MsgInfo?
|
||||
}
|
||||
|
||||
public struct QSub: Codable {
|
||||
public struct QSub: Codable, Hashable {
|
||||
var qSubThread: QSubThread
|
||||
var qDelivered: String?
|
||||
}
|
||||
|
||||
public enum QSubThread: String, Codable {
|
||||
public enum QSubThread: String, Codable, Hashable {
|
||||
case noSub
|
||||
case subPending
|
||||
case subThread
|
||||
case prohibitSub
|
||||
}
|
||||
|
||||
public struct MsgInfo: Codable {
|
||||
public struct MsgInfo: Codable, Hashable {
|
||||
var msgId: String
|
||||
var msgTs: Date
|
||||
var msgType: MsgType
|
||||
}
|
||||
|
||||
public enum MsgType: String, Codable {
|
||||
public enum MsgType: String, Codable, Hashable {
|
||||
case message
|
||||
case quota
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public struct User: Identifiable, Decodable, UserLike, NamedChat {
|
||||
public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
|
||||
public var userId: Int64
|
||||
var userContactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
@@ -52,7 +52,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct UserRef: Identifiable, Decodable, UserLike {
|
||||
public struct UserRef: Identifiable, Decodable, UserLike, Hashable {
|
||||
public var userId: Int64
|
||||
public var localDisplayName: ContactName
|
||||
|
||||
@@ -63,12 +63,12 @@ public protocol UserLike: Identifiable {
|
||||
var userId: Int64 { get }
|
||||
}
|
||||
|
||||
public struct UserPwdHash: Decodable {
|
||||
public struct UserPwdHash: Decodable, Hashable {
|
||||
public var hash: String
|
||||
public var salt: String
|
||||
}
|
||||
|
||||
public struct UserInfo: Decodable, Identifiable {
|
||||
public struct UserInfo: Decodable, Identifiable, Hashable {
|
||||
public var user: User
|
||||
public var unreadCount: Int
|
||||
|
||||
@@ -89,7 +89,7 @@ public typealias ContactName = String
|
||||
|
||||
public typealias GroupName = String
|
||||
|
||||
public struct Profile: Codable, NamedChat {
|
||||
public struct Profile: Codable, NamedChat, Hashable {
|
||||
public init(
|
||||
displayName: String,
|
||||
fullName: String,
|
||||
@@ -121,7 +121,7 @@ public struct Profile: Codable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct LocalProfile: Codable, NamedChat {
|
||||
public struct LocalProfile: Codable, NamedChat, Hashable {
|
||||
public init(
|
||||
profileId: Int64,
|
||||
displayName: String,
|
||||
@@ -171,13 +171,13 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile {
|
||||
Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences)
|
||||
}
|
||||
|
||||
public struct UserProfileUpdateSummary: Decodable {
|
||||
public struct UserProfileUpdateSummary: Decodable, Hashable {
|
||||
public var updateSuccesses: Int
|
||||
public var updateFailures: Int
|
||||
public var changedContacts: [Contact]
|
||||
}
|
||||
|
||||
public enum ChatType: String {
|
||||
public enum ChatType: String, Hashable {
|
||||
case direct = "@"
|
||||
case group = "#"
|
||||
case local = "*"
|
||||
@@ -202,7 +202,7 @@ extension NamedChat {
|
||||
|
||||
public typealias ChatId = String
|
||||
|
||||
public struct FullPreferences: Decodable, Equatable {
|
||||
public struct FullPreferences: Decodable, Equatable, Hashable {
|
||||
public var timedMessages: TimedMessagesPreference
|
||||
public var fullDelete: SimplePreference
|
||||
public var reactions: SimplePreference
|
||||
@@ -232,7 +232,7 @@ public struct FullPreferences: Decodable, Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct Preferences: Codable {
|
||||
public struct Preferences: Codable, Hashable {
|
||||
public var timedMessages: TimedMessagesPreference?
|
||||
public var fullDelete: SimplePreference?
|
||||
public var reactions: SimplePreference?
|
||||
@@ -308,11 +308,11 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac
|
||||
)
|
||||
}
|
||||
|
||||
public protocol Preference: Codable, Equatable {
|
||||
public protocol Preference: Codable, Equatable, Hashable {
|
||||
var allow: FeatureAllowed { get set }
|
||||
}
|
||||
|
||||
public struct SimplePreference: Preference {
|
||||
public struct SimplePreference: Preference, Hashable {
|
||||
public var allow: FeatureAllowed
|
||||
|
||||
public init(allow: FeatureAllowed) {
|
||||
@@ -320,7 +320,7 @@ public struct SimplePreference: Preference {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TimedMessagesPreference: Preference {
|
||||
public struct TimedMessagesPreference: Preference, Hashable {
|
||||
public var allow: FeatureAllowed
|
||||
public var ttl: Int?
|
||||
|
||||
@@ -334,7 +334,7 @@ public struct TimedMessagesPreference: Preference {
|
||||
}
|
||||
}
|
||||
|
||||
public enum CustomTimeUnit {
|
||||
public enum CustomTimeUnit: Hashable {
|
||||
case second
|
||||
case minute
|
||||
case hour
|
||||
@@ -433,7 +433,7 @@ public func shortTimeText(_ seconds: Int?) -> LocalizedStringKey {
|
||||
return CustomTimeUnit.toShortText(seconds: seconds)
|
||||
}
|
||||
|
||||
public struct ContactUserPreferences: Decodable {
|
||||
public struct ContactUserPreferences: Decodable, Hashable {
|
||||
public var timedMessages: ContactUserPreference<TimedMessagesPreference>
|
||||
public var fullDelete: ContactUserPreference<SimplePreference>
|
||||
public var reactions: ContactUserPreference<SimplePreference>
|
||||
@@ -483,7 +483,7 @@ public struct ContactUserPreferences: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct ContactUserPreference<P: Preference>: Decodable {
|
||||
public struct ContactUserPreference<P: Preference>: Decodable, Hashable {
|
||||
public var enabled: FeatureEnabled
|
||||
public var userPreference: ContactUserPref<P>
|
||||
public var contactPreference: P
|
||||
@@ -495,7 +495,7 @@ public struct ContactUserPreference<P: Preference>: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct FeatureEnabled: Decodable {
|
||||
public struct FeatureEnabled: Decodable, Hashable {
|
||||
public var forUser: Bool
|
||||
public var forContact: Bool
|
||||
|
||||
@@ -526,7 +526,7 @@ public struct FeatureEnabled: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ContactUserPref<P: Preference>: Decodable {
|
||||
public enum ContactUserPref<P: Preference>: Decodable, Hashable {
|
||||
case contact(preference: P) // contact override is set
|
||||
case user(preference: P) // global user default is used
|
||||
|
||||
@@ -547,7 +547,7 @@ public protocol Feature {
|
||||
var text: String { get }
|
||||
}
|
||||
|
||||
public enum ChatFeature: String, Decodable, Feature {
|
||||
public enum ChatFeature: String, Decodable, Feature, Hashable {
|
||||
case timedMessages
|
||||
case fullDelete
|
||||
case reactions
|
||||
@@ -690,7 +690,7 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupFeature: String, Decodable, Feature {
|
||||
public enum GroupFeature: String, Decodable, Feature, Hashable {
|
||||
case timedMessages
|
||||
case directMessages
|
||||
case fullDelete
|
||||
@@ -890,7 +890,7 @@ public enum ContactFeatureAllowed: Identifiable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ContactFeaturesAllowed: Equatable {
|
||||
public struct ContactFeaturesAllowed: Equatable, Hashable {
|
||||
public var timedMessagesAllowed: Bool
|
||||
public var timedMessagesTTL: Int?
|
||||
public var fullDelete: ContactFeatureAllowed
|
||||
@@ -968,7 +968,7 @@ public func contactFeatureAllowedToPref(_ contactFeatureAllowed: ContactFeatureA
|
||||
}
|
||||
}
|
||||
|
||||
public enum FeatureAllowed: String, Codable, Identifiable {
|
||||
public enum FeatureAllowed: String, Codable, Identifiable, Hashable {
|
||||
case always
|
||||
case yes
|
||||
case no
|
||||
@@ -986,7 +986,7 @@ public enum FeatureAllowed: String, Codable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct FullGroupPreferences: Decodable, Equatable {
|
||||
public struct FullGroupPreferences: Decodable, Equatable, Hashable {
|
||||
public var timedMessages: TimedMessagesGroupPreference
|
||||
public var directMessages: RoleGroupPreference
|
||||
public var fullDelete: GroupPreference
|
||||
@@ -1028,7 +1028,7 @@ public struct FullGroupPreferences: Decodable, Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupPreferences: Codable {
|
||||
public struct GroupPreferences: Codable, Hashable {
|
||||
public var timedMessages: TimedMessagesGroupPreference?
|
||||
public var directMessages: RoleGroupPreference?
|
||||
public var fullDelete: GroupPreference?
|
||||
@@ -1083,7 +1083,7 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupPreference: Codable, Equatable {
|
||||
public struct GroupPreference: Codable, Equatable, Hashable {
|
||||
public var enable: GroupFeatureEnabled
|
||||
|
||||
public var on: Bool {
|
||||
@@ -1107,7 +1107,7 @@ public struct GroupPreference: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RoleGroupPreference: Codable, Equatable {
|
||||
public struct RoleGroupPreference: Codable, Equatable, Hashable {
|
||||
public var enable: GroupFeatureEnabled
|
||||
public var role: GroupMemberRole?
|
||||
|
||||
@@ -1121,7 +1121,7 @@ public struct RoleGroupPreference: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TimedMessagesGroupPreference: Codable, Equatable {
|
||||
public struct TimedMessagesGroupPreference: Codable, Equatable, Hashable {
|
||||
public var enable: GroupFeatureEnabled
|
||||
public var ttl: Int?
|
||||
|
||||
@@ -1135,7 +1135,7 @@ public struct TimedMessagesGroupPreference: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupFeatureEnabled: String, Codable, Identifiable {
|
||||
public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable {
|
||||
case on
|
||||
case off
|
||||
|
||||
@@ -1158,7 +1158,7 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
case direct(contact: Contact)
|
||||
case group(groupInfo: GroupInfo)
|
||||
case local(noteFolder: NoteFolder)
|
||||
@@ -1370,7 +1370,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ShowEnableVoiceMessagesAlert {
|
||||
public enum ShowEnableVoiceMessagesAlert: Hashable {
|
||||
case userEnable
|
||||
case askContact
|
||||
case groupOwnerCan
|
||||
@@ -1443,7 +1443,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SampleData {
|
||||
public struct SampleData: Hashable {
|
||||
public var direct: ChatInfo
|
||||
public var group: ChatInfo
|
||||
public var local: ChatInfo
|
||||
@@ -1460,7 +1460,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct ChatData: Decodable, Identifiable {
|
||||
public struct ChatData: Decodable, Identifiable, Hashable {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatItems: [ChatItem]
|
||||
public var chatStats: ChatStats
|
||||
@@ -1476,7 +1476,7 @@ public struct ChatData: Decodable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatStats: Decodable {
|
||||
public struct ChatStats: Decodable, Hashable {
|
||||
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) {
|
||||
self.unreadCount = unreadCount
|
||||
self.minUnreadItemId = minUnreadItemId
|
||||
@@ -1488,7 +1488,7 @@ public struct ChatStats: Decodable {
|
||||
public var unreadChat: Bool = false
|
||||
}
|
||||
|
||||
public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var contactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
public var profile: LocalProfile
|
||||
@@ -1574,12 +1574,12 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public enum ContactStatus: String, Decodable {
|
||||
public enum ContactStatus: String, Decodable, Hashable {
|
||||
case active = "active"
|
||||
case deleted = "deleted"
|
||||
}
|
||||
|
||||
public struct ContactRef: Decodable, Equatable {
|
||||
public struct ContactRef: Decodable, Equatable, Hashable {
|
||||
var contactId: Int64
|
||||
public var agentConnId: String
|
||||
var connId: Int64
|
||||
@@ -1588,12 +1588,12 @@ public struct ContactRef: Decodable, Equatable {
|
||||
public var id: ChatId { get { "@\(contactId)" } }
|
||||
}
|
||||
|
||||
public struct ContactSubStatus: Decodable {
|
||||
public struct ContactSubStatus: Decodable, Hashable {
|
||||
public var contact: Contact
|
||||
public var contactError: ChatError?
|
||||
}
|
||||
|
||||
public struct Connection: Decodable {
|
||||
public struct Connection: Decodable, Hashable {
|
||||
public var connId: Int64
|
||||
public var agentConnId: String
|
||||
public var peerChatVRange: VersionRange
|
||||
@@ -1637,7 +1637,7 @@ public struct Connection: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct VersionRange: Decodable {
|
||||
public struct VersionRange: Decodable, Hashable {
|
||||
public init(minVersion: Int, maxVersion: Int) {
|
||||
self.minVersion = minVersion
|
||||
self.maxVersion = maxVersion
|
||||
@@ -1651,7 +1651,7 @@ public struct VersionRange: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SecurityCode: Decodable, Equatable {
|
||||
public struct SecurityCode: Decodable, Equatable, Hashable {
|
||||
public init(securityCode: String, verifiedAt: Date) {
|
||||
self.securityCode = securityCode
|
||||
self.verifiedAt = verifiedAt
|
||||
@@ -1661,7 +1661,7 @@ public struct SecurityCode: Decodable, Equatable {
|
||||
public var verifiedAt: Date
|
||||
}
|
||||
|
||||
public struct UserContact: Decodable {
|
||||
public struct UserContact: Decodable, Hashable {
|
||||
public var userContactLinkId: Int64
|
||||
// public var connReqContact: String
|
||||
public var groupId: Int64?
|
||||
@@ -1679,7 +1679,7 @@ public struct UserContact: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct UserContactRequest: Decodable, NamedChat {
|
||||
public struct UserContactRequest: Decodable, NamedChat, Hashable {
|
||||
var contactRequestId: Int64
|
||||
public var userContactLinkId: Int64
|
||||
public var cReqChatVRange: VersionRange
|
||||
@@ -1708,7 +1708,7 @@ public struct UserContactRequest: Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct PendingContactConnection: Decodable, NamedChat {
|
||||
public struct PendingContactConnection: Decodable, NamedChat, Hashable {
|
||||
public var pccConnId: Int64
|
||||
var pccAgentConnId: String
|
||||
var pccConnStatus: ConnStatus
|
||||
@@ -1798,7 +1798,7 @@ public struct PendingContactConnection: Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConnStatus: String, Decodable {
|
||||
public enum ConnStatus: String, Decodable, Hashable {
|
||||
case new = "new"
|
||||
case joined = "joined"
|
||||
case requested = "requested"
|
||||
@@ -1822,7 +1822,7 @@ public enum ConnStatus: String, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Group: Decodable {
|
||||
public struct Group: Decodable, Hashable {
|
||||
public var groupInfo: GroupInfo
|
||||
public var members: [GroupMember]
|
||||
|
||||
@@ -1832,7 +1832,7 @@ public struct Group: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var groupId: Int64
|
||||
var localDisplayName: GroupName
|
||||
public var groupProfile: GroupProfile
|
||||
@@ -1878,12 +1878,12 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupRef: Decodable {
|
||||
public struct GroupRef: Decodable, Hashable {
|
||||
public var groupId: Int64
|
||||
var localDisplayName: GroupName
|
||||
}
|
||||
|
||||
public struct GroupProfile: Codable, NamedChat {
|
||||
public struct GroupProfile: Codable, NamedChat, Hashable {
|
||||
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
|
||||
self.displayName = displayName
|
||||
self.fullName = fullName
|
||||
@@ -1905,7 +1905,7 @@ public struct GroupProfile: Codable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupMember: Identifiable, Decodable {
|
||||
public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
public var groupMemberId: Int64
|
||||
public var groupId: Int64
|
||||
public var memberId: String
|
||||
@@ -2037,21 +2037,21 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupMemberSettings: Codable {
|
||||
public struct GroupMemberSettings: Codable, Hashable {
|
||||
public var showMessages: Bool
|
||||
}
|
||||
|
||||
public struct GroupMemberRef: Decodable {
|
||||
public struct GroupMemberRef: Decodable, Hashable {
|
||||
var groupMemberId: Int64
|
||||
var profile: Profile
|
||||
}
|
||||
|
||||
public struct GroupMemberIds: Decodable {
|
||||
public struct GroupMemberIds: Decodable, Hashable {
|
||||
var groupMemberId: Int64
|
||||
var groupId: Int64
|
||||
}
|
||||
|
||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable {
|
||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable, Hashable {
|
||||
case observer = "observer"
|
||||
case author = "author"
|
||||
case member = "member"
|
||||
@@ -2085,7 +2085,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupMemberCategory: String, Decodable {
|
||||
public enum GroupMemberCategory: String, Decodable, Hashable {
|
||||
case userMember = "user"
|
||||
case inviteeMember = "invitee"
|
||||
case hostMember = "host"
|
||||
@@ -2093,7 +2093,7 @@ public enum GroupMemberCategory: String, Decodable {
|
||||
case postMember = "post"
|
||||
}
|
||||
|
||||
public enum GroupMemberStatus: String, Decodable {
|
||||
public enum GroupMemberStatus: String, Decodable, Hashable {
|
||||
case memRemoved = "removed"
|
||||
case memLeft = "left"
|
||||
case memGroupDeleted = "deleted"
|
||||
@@ -2142,7 +2142,7 @@ public enum GroupMemberStatus: String, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct NoteFolder: Identifiable, Decodable, NamedChat {
|
||||
public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var noteFolderId: Int64
|
||||
public var favorite: Bool
|
||||
public var unread: Bool
|
||||
@@ -2175,18 +2175,18 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public enum InvitedBy: Decodable {
|
||||
public enum InvitedBy: Decodable, Hashable {
|
||||
case contact(byContactId: Int64)
|
||||
case user
|
||||
case unknown
|
||||
}
|
||||
|
||||
public struct MemberSubError: Decodable {
|
||||
public struct MemberSubError: Decodable, Hashable {
|
||||
var member: GroupMemberIds
|
||||
var memberError: ChatError
|
||||
}
|
||||
|
||||
public enum ConnectionEntity: Decodable {
|
||||
public enum ConnectionEntity: Decodable, Hashable {
|
||||
case rcvDirectMsgConnection(contact: Contact?)
|
||||
case rcvGroupMsgConnection(groupInfo: GroupInfo, groupMember: GroupMember)
|
||||
case sndFileConnection(sndFileTransfer: SndFileTransfer)
|
||||
@@ -2217,12 +2217,12 @@ public enum ConnectionEntity: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct NtfMsgInfo: Decodable {
|
||||
public struct NtfMsgInfo: Decodable, Hashable {
|
||||
public var msgId: String
|
||||
public var msgTs: Date
|
||||
}
|
||||
|
||||
public struct AChatItem: Decodable {
|
||||
public struct AChatItem: Decodable, Hashable {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatItem: ChatItem
|
||||
|
||||
@@ -2234,19 +2234,19 @@ public struct AChatItem: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ACIReaction: Decodable {
|
||||
public struct ACIReaction: Decodable, Hashable {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatReaction: CIReaction
|
||||
}
|
||||
|
||||
public struct CIReaction: Decodable {
|
||||
public struct CIReaction: Decodable, Hashable {
|
||||
public var chatDir: CIDirection
|
||||
public var chatItem: ChatItem
|
||||
public var sentAt: Date
|
||||
public var reaction: MsgReaction
|
||||
}
|
||||
|
||||
public struct ChatItem: Identifiable, Decodable {
|
||||
public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) {
|
||||
self.chatDir = chatDir
|
||||
self.meta = meta
|
||||
@@ -2596,7 +2596,7 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum CIMergeCategory {
|
||||
public enum CIMergeCategory: Hashable {
|
||||
case memberConnected
|
||||
case rcvGroupEvent
|
||||
case sndGroupEvent
|
||||
@@ -2605,7 +2605,7 @@ public enum CIMergeCategory {
|
||||
case chatFeature
|
||||
}
|
||||
|
||||
public enum CIDirection: Decodable {
|
||||
public enum CIDirection: Decodable, Hashable {
|
||||
case directSnd
|
||||
case directRcv
|
||||
case groupSnd
|
||||
@@ -2627,7 +2627,7 @@ public enum CIDirection: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIMeta: Decodable {
|
||||
public struct CIMeta: Decodable, Hashable {
|
||||
public var itemId: Int64
|
||||
public var itemTs: Date
|
||||
var itemText: String
|
||||
@@ -2690,7 +2690,7 @@ public struct CIMeta: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CITimed: Decodable {
|
||||
public struct CITimed: Decodable, Hashable {
|
||||
public var ttl: Int
|
||||
public var deleteAt: Date?
|
||||
}
|
||||
@@ -2717,7 +2717,7 @@ private func recent(_ date: Date) -> Bool {
|
||||
return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00)
|
||||
}
|
||||
|
||||
public enum CIStatus: Decodable {
|
||||
public enum CIStatus: Decodable, Hashable {
|
||||
case sndNew
|
||||
case sndSent(sndProgress: SndCIStatusProgress)
|
||||
case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress)
|
||||
@@ -2787,7 +2787,7 @@ public enum CIStatus: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndError: Decodable {
|
||||
public enum SndError: Decodable, Hashable {
|
||||
case auth
|
||||
case quota
|
||||
case expired
|
||||
@@ -2809,7 +2809,7 @@ public enum SndError: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SrvError: Decodable, Equatable {
|
||||
public enum SrvError: Decodable, Hashable {
|
||||
case host
|
||||
case version
|
||||
case other(srvError: String)
|
||||
@@ -2831,17 +2831,17 @@ public enum SrvError: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgReceiptStatus: String, Decodable {
|
||||
public enum MsgReceiptStatus: String, Decodable, Hashable {
|
||||
case ok
|
||||
case badMsgHash
|
||||
}
|
||||
|
||||
public enum SndCIStatusProgress: String, Decodable {
|
||||
public enum SndCIStatusProgress: String, Decodable, Hashable {
|
||||
case partial
|
||||
case complete
|
||||
}
|
||||
|
||||
public enum CIDeleted: Decodable {
|
||||
public enum CIDeleted: Decodable, Hashable {
|
||||
case deleted(deletedTs: Date?)
|
||||
case blocked(deletedTs: Date?)
|
||||
case blockedByAdmin(deletedTs: Date?)
|
||||
@@ -2857,12 +2857,12 @@ public enum CIDeleted: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgDirection: String, Decodable {
|
||||
public enum MsgDirection: String, Decodable, Hashable {
|
||||
case rcv = "rcv"
|
||||
case snd = "snd"
|
||||
}
|
||||
|
||||
public enum CIForwardedFrom: Decodable {
|
||||
public enum CIForwardedFrom: Decodable, Hashable {
|
||||
case unknown
|
||||
case contact(chatName: String, msgDir: MsgDirection, contactId: Int64?, chatItemId: Int64?)
|
||||
case group(chatName: String, msgDir: MsgDirection, groupId: Int64?, chatItemId: Int64?)
|
||||
@@ -2882,7 +2882,7 @@ public enum CIForwardedFrom: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum CIDeleteMode: String, Decodable {
|
||||
public enum CIDeleteMode: String, Decodable, Hashable {
|
||||
case cidmBroadcast = "broadcast"
|
||||
case cidmInternal = "internal"
|
||||
}
|
||||
@@ -2891,7 +2891,7 @@ protocol ItemContent {
|
||||
var text: String { get }
|
||||
}
|
||||
|
||||
public enum CIContent: Decodable, ItemContent {
|
||||
public enum CIContent: Decodable, ItemContent, Hashable {
|
||||
case sndMsgContent(msgContent: MsgContent)
|
||||
case rcvMsgContent(msgContent: MsgContent)
|
||||
case sndDeleted(deleteMode: CIDeleteMode) // legacy - since v4.3.0 itemDeleted field is used
|
||||
@@ -3027,7 +3027,7 @@ public enum CIContent: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgDecryptError: String, Decodable {
|
||||
public enum MsgDecryptError: String, Decodable, Hashable {
|
||||
case ratchetHeader
|
||||
case tooManySkipped
|
||||
case ratchetEarlier
|
||||
@@ -3045,7 +3045,7 @@ public enum MsgDecryptError: String, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIQuote: Decodable, ItemContent {
|
||||
public struct CIQuote: Decodable, ItemContent, Hashable {
|
||||
public var chatDir: CIDirection?
|
||||
public var itemId: Int64?
|
||||
var sharedMsgId: String? = nil
|
||||
@@ -3083,13 +3083,13 @@ public struct CIQuote: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIReactionCount: Decodable {
|
||||
public struct CIReactionCount: Decodable, Hashable {
|
||||
public var reaction: MsgReaction
|
||||
public var userReacted: Bool
|
||||
public var totalReacted: Int
|
||||
}
|
||||
|
||||
public enum MsgReaction: Hashable {
|
||||
public enum MsgReaction: Hashable, Identifiable {
|
||||
case emoji(emoji: MREmojiChar)
|
||||
case unknown(type: String)
|
||||
|
||||
@@ -3110,9 +3110,16 @@ public enum MsgReaction: Hashable {
|
||||
case type
|
||||
case emoji
|
||||
}
|
||||
|
||||
public var id: String {
|
||||
switch self {
|
||||
case let .emoji(emoji): emoji.rawValue
|
||||
case let .unknown(unknown): unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MREmojiChar: String, Codable, CaseIterable {
|
||||
public enum MREmojiChar: String, Codable, CaseIterable, Hashable {
|
||||
case thumbsup = "👍"
|
||||
case thumbsdown = "👎"
|
||||
case smile = "😀"
|
||||
@@ -3153,7 +3160,7 @@ extension MsgReaction: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIFile: Decodable {
|
||||
public struct CIFile: Decodable, Hashable {
|
||||
public var fileId: Int64
|
||||
public var fileName: String
|
||||
public var fileSize: Int64
|
||||
@@ -3221,7 +3228,7 @@ public struct CIFile: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CryptoFile: Codable {
|
||||
public struct CryptoFile: Codable, Hashable {
|
||||
public var filePath: String // the name of the file, not a full path
|
||||
public var cryptoArgs: CryptoFileArgs?
|
||||
|
||||
@@ -3268,22 +3275,28 @@ public struct CryptoFile: Codable {
|
||||
static var decryptedUrls = Dictionary<String, URL>()
|
||||
}
|
||||
|
||||
public struct CryptoFileArgs: Codable {
|
||||
public struct CryptoFileArgs: Codable, Hashable {
|
||||
public var fileKey: String
|
||||
public var fileNonce: String
|
||||
}
|
||||
|
||||
public struct CancelAction {
|
||||
public struct CancelAction: Hashable {
|
||||
public var uiAction: String
|
||||
public var alert: AlertInfo
|
||||
}
|
||||
|
||||
public struct AlertInfo {
|
||||
public struct AlertInfo: Hashable {
|
||||
public var title: LocalizedStringKey
|
||||
public var message: LocalizedStringKey
|
||||
public var confirm: LocalizedStringKey
|
||||
}
|
||||
|
||||
extension LocalizedStringKey: Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine("\(self)")
|
||||
}
|
||||
}
|
||||
|
||||
private var sndCancelAction = CancelAction(
|
||||
uiAction: NSLocalizedString("Stop file", comment: "cancel file action"),
|
||||
alert: AlertInfo(
|
||||
@@ -3311,13 +3324,13 @@ private var rcvCancelAction = CancelAction(
|
||||
)
|
||||
)
|
||||
|
||||
public enum FileProtocol: String, Decodable {
|
||||
public enum FileProtocol: String, Decodable, Hashable {
|
||||
case smp = "smp"
|
||||
case xftp = "xftp"
|
||||
case local = "local"
|
||||
}
|
||||
|
||||
public enum CIFileStatus: Decodable, Equatable {
|
||||
public enum CIFileStatus: Decodable, Equatable, Hashable {
|
||||
case sndStored
|
||||
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
|
||||
case sndComplete
|
||||
@@ -3355,7 +3368,7 @@ public enum CIFileStatus: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FileError: Decodable, Equatable {
|
||||
public enum FileError: Decodable, Equatable, Hashable {
|
||||
case auth
|
||||
case noFile
|
||||
case relay(srvError: SrvError)
|
||||
@@ -3380,7 +3393,7 @@ public enum FileError: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgContent: Equatable {
|
||||
public enum MsgContent: Equatable, Hashable {
|
||||
case text(String)
|
||||
case link(text: String, preview: LinkPreview)
|
||||
case image(text: String, image: String)
|
||||
@@ -3547,7 +3560,7 @@ extension MsgContent: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct FormattedText: Decodable {
|
||||
public struct FormattedText: Decodable, Hashable {
|
||||
public var text: String
|
||||
public var format: Format?
|
||||
|
||||
@@ -3556,7 +3569,7 @@ public struct FormattedText: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Format: Decodable, Equatable {
|
||||
public enum Format: Decodable, Equatable, Hashable {
|
||||
case bold
|
||||
case italic
|
||||
case strikeThrough
|
||||
@@ -3578,7 +3591,7 @@ public enum Format: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SimplexLinkType: String, Decodable {
|
||||
public enum SimplexLinkType: String, Decodable, Hashable {
|
||||
case contact
|
||||
case invitation
|
||||
case group
|
||||
@@ -3592,7 +3605,7 @@ public enum SimplexLinkType: String, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FormatColor: String, Decodable {
|
||||
public enum FormatColor: String, Decodable, Hashable {
|
||||
case red = "red"
|
||||
case green = "green"
|
||||
case blue = "blue"
|
||||
@@ -3619,7 +3632,7 @@ public enum FormatColor: String, Decodable {
|
||||
}
|
||||
|
||||
// Struct to use with simplex API
|
||||
public struct LinkPreview: Codable, Equatable {
|
||||
public struct LinkPreview: Codable, Equatable, Hashable {
|
||||
public init(uri: URL, title: String, description: String = "", image: String) {
|
||||
self.uri = uri
|
||||
self.title = title
|
||||
@@ -3634,7 +3647,7 @@ public struct LinkPreview: Codable, Equatable {
|
||||
public var image: String
|
||||
}
|
||||
|
||||
public enum NtfTknStatus: String, Decodable {
|
||||
public enum NtfTknStatus: String, Decodable, Hashable {
|
||||
case new = "NEW"
|
||||
case registered = "REGISTERED"
|
||||
case invalid = "INVALID"
|
||||
@@ -3643,22 +3656,22 @@ public enum NtfTknStatus: String, Decodable {
|
||||
case expired = "EXPIRED"
|
||||
}
|
||||
|
||||
public struct SndFileTransfer: Decodable {
|
||||
public struct SndFileTransfer: Decodable, Hashable {
|
||||
|
||||
}
|
||||
|
||||
public struct RcvFileTransfer: Decodable {
|
||||
public struct RcvFileTransfer: Decodable, Hashable {
|
||||
public let fileId: Int64
|
||||
}
|
||||
|
||||
public struct FileTransferMeta: Decodable {
|
||||
public struct FileTransferMeta: Decodable, Hashable {
|
||||
public let fileId: Int64
|
||||
public let fileName: String
|
||||
public let filePath: String
|
||||
public let fileSize: Int64
|
||||
}
|
||||
|
||||
public enum CICallStatus: String, Decodable {
|
||||
public enum CICallStatus: String, Decodable, Hashable {
|
||||
case pending
|
||||
case missed
|
||||
case rejected
|
||||
@@ -3690,7 +3703,7 @@ public func durationText(_ sec: Int) -> String {
|
||||
: String(format: "%02d:%02d:%02d", m / 60, m % 60, s)
|
||||
}
|
||||
|
||||
public enum MsgErrorType: Decodable {
|
||||
public enum MsgErrorType: Decodable, Hashable {
|
||||
case msgSkipped(fromMsgId: Int64, toMsgId: Int64)
|
||||
case msgBadId(msgId: Int64)
|
||||
case msgBadHash
|
||||
@@ -3707,7 +3720,7 @@ public enum MsgErrorType: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIGroupInvitation: Decodable {
|
||||
public struct CIGroupInvitation: Decodable, Hashable {
|
||||
public var groupId: Int64
|
||||
public var groupMemberId: Int64
|
||||
public var localDisplayName: GroupName
|
||||
@@ -3723,18 +3736,18 @@ public struct CIGroupInvitation: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum CIGroupInvitationStatus: String, Decodable {
|
||||
public enum CIGroupInvitationStatus: String, Decodable, Hashable {
|
||||
case pending
|
||||
case accepted
|
||||
case rejected
|
||||
case expired
|
||||
}
|
||||
|
||||
public struct E2EEInfo: Decodable {
|
||||
public struct E2EEInfo: Decodable, Hashable {
|
||||
public var pqEnabled: Bool
|
||||
}
|
||||
|
||||
public enum RcvDirectEvent: Decodable {
|
||||
public enum RcvDirectEvent: Decodable, Hashable {
|
||||
case contactDeleted
|
||||
case profileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
|
||||
@@ -3763,7 +3776,7 @@ public enum RcvDirectEvent: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RcvGroupEvent: Decodable {
|
||||
public enum RcvGroupEvent: Decodable, Hashable {
|
||||
case memberAdded(groupMemberId: Int64, profile: Profile)
|
||||
case memberConnected
|
||||
case memberLeft
|
||||
@@ -3819,7 +3832,7 @@ public enum RcvGroupEvent: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndGroupEvent: Decodable {
|
||||
public enum SndGroupEvent: Decodable, Hashable {
|
||||
case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole)
|
||||
case userRole(role: GroupMemberRole)
|
||||
case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool)
|
||||
@@ -3847,7 +3860,7 @@ public enum SndGroupEvent: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RcvConnEvent: Decodable {
|
||||
public enum RcvConnEvent: Decodable, Hashable {
|
||||
case switchQueue(phase: SwitchPhase)
|
||||
case ratchetSync(syncStatus: RatchetSyncState)
|
||||
case verificationCodeReset
|
||||
@@ -3884,7 +3897,7 @@ func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndConnEvent: Decodable {
|
||||
public enum SndConnEvent: Decodable, Hashable {
|
||||
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
|
||||
case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?)
|
||||
case pqEnabled(enabled: Bool)
|
||||
@@ -3921,14 +3934,14 @@ public enum SndConnEvent: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SwitchPhase: String, Decodable {
|
||||
public enum SwitchPhase: String, Decodable, Hashable {
|
||||
case started
|
||||
case confirmed
|
||||
case secured
|
||||
case completed
|
||||
}
|
||||
|
||||
public enum ChatItemTTL: Hashable, Identifiable, Comparable {
|
||||
public enum ChatItemTTL: Identifiable, Comparable, Hashable {
|
||||
case day
|
||||
case week
|
||||
case month
|
||||
@@ -3978,13 +3991,13 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatItemInfo: Decodable {
|
||||
public struct ChatItemInfo: Decodable, Hashable {
|
||||
public var itemVersions: [ChatItemVersion]
|
||||
public var memberDeliveryStatuses: [MemberDeliveryStatus]?
|
||||
public var forwardedFromChatItem: AChatItem?
|
||||
}
|
||||
|
||||
public struct ChatItemVersion: Decodable {
|
||||
public struct ChatItemVersion: Decodable, Hashable {
|
||||
public var chatItemVersionId: Int64
|
||||
public var msgContent: MsgContent
|
||||
public var formattedText: [FormattedText]?
|
||||
@@ -3992,7 +4005,7 @@ public struct ChatItemVersion: Decodable {
|
||||
public var createdAt: Date
|
||||
}
|
||||
|
||||
public struct MemberDeliveryStatus: Decodable {
|
||||
public struct MemberDeliveryStatus: Decodable, Hashable {
|
||||
public var groupMemberId: Int64
|
||||
public var memberDeliveryStatus: CIStatus
|
||||
public var sentViaProxy: Bool?
|
||||
|
||||
Reference in New Issue
Block a user