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:
Evgeny Poberezkin
2024-07-03 10:24:26 +01:00
committed by GitHub
parent e22e01acd2
commit 2585f4ecfd
14 changed files with 921 additions and 649 deletions
+10 -8
View File
@@ -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
}
+3 -1
View File
@@ -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
+31 -4
View File
@@ -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")
// }
// }
}
}
+4 -4
View File
@@ -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 */,
+68 -68
View File
@@ -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
}
+129 -116
View File
@@ -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?