From ae1365411eabfb82cf0e5e1043e79c14d5ddc363 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Fri, 19 Jul 2024 08:03:57 -0700 Subject: [PATCH] ios: interactive media and link previews in the list of chats --- .../Chat/ChatItem/AnimatedImageView.swift | 14 +- .../Views/Chat/ChatItem/CIFileView.swift | 66 +++++---- .../Views/Chat/ChatItem/CIImageView.swift | 22 ++- .../Views/Chat/ChatItem/CIVideoView.swift | 106 +++++++++++--- .../Views/Chat/ChatItem/CIVoiceView.swift | 19 ++- .../Views/ChatList/ChatPreviewView.swift | 132 +++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 49 ------- apps/ios/SimpleXChat/ChatTypes.swift | 22 +++ 8 files changed, 323 insertions(+), 107 deletions(-) delete mode 100644 apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift index bcdeb7fd9c..30f5e7a589 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift @@ -9,6 +9,7 @@ import SwiftUI class AnimatedImageView: UIView { var image: UIImage? = nil var imageView: UIImageView? = nil + var cMode: UIView.ContentMode = .scaleAspectFit override init(frame: CGRect) { super.init(frame: frame) @@ -18,11 +19,12 @@ class AnimatedImageView: UIView { fatalError("Not implemented") } - convenience init(image: UIImage) { + convenience init(image: UIImage, contentMode: UIView.ContentMode) { self.init() self.image = image + self.cMode = contentMode imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) } @@ -35,7 +37,7 @@ class AnimatedImageView: UIView { if let subview = self.subviews.first as? UIImageView { if image.imageData != subview.gifImage?.imageData { imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) subview.removeFromSuperview() } @@ -47,13 +49,15 @@ class AnimatedImageView: UIView { struct SwiftyGif: UIViewRepresentable { private let image: UIImage + private let contentMode: UIView.ContentMode - init(image: UIImage) { + init(image: UIImage, contentMode: UIView.ContentMode = .scaleAspectFit) { self.image = image + self.contentMode = contentMode } func makeUIView(context: Context) -> AnimatedImageView { - AnimatedImageView(image: image) + AnimatedImageView(image: image, contentMode: contentMode) } func updateUIView(_ imageView: AnimatedImageView, context: Context) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 414da5371a..57123d74ba 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -14,39 +14,45 @@ struct CIFileView: View { @EnvironmentObject var theme: AppTheme let file: CIFile? let edited: Bool + var smallView: Bool = false var body: some View { - let metaReserve = edited - ? " " - : " " - Button(action: fileAction) { - HStack(alignment: .bottom, spacing: 6) { - fileIndicator() - .padding(.top, 5) - .padding(.bottom, 3) - if let file = file { - let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) - VStack(alignment: .leading, spacing: 2) { - Text(file.fileName) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(theme.colors.onBackground) - Text(prettyFileSize + metaReserve) - .font(.caption) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(theme.colors.secondary) + if smallView { + fileIndicator() + .onTapGesture(perform: fileAction) + } else { + let metaReserve = edited + ? " " + : " " + Button(action: fileAction) { + HStack(alignment: .bottom, spacing: 6) { + fileIndicator() + .padding(.top, 5) + .padding(.bottom, 3) + if let file = file { + let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) + VStack(alignment: .leading, spacing: 2) { + Text(file.fileName) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.onBackground) + Text(prettyFileSize + metaReserve) + .font(.caption) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.secondary) + } + } else { + Text(metaReserve) } - } else { - Text(metaReserve) } + .padding(.top, 4) + .padding(.bottom, 6) + .padding(.leading, 10) + .padding(.trailing, 12) } - .padding(.top, 4) - .padding(.bottom, 6) - .padding(.leading, 10) - .padding(.trailing, 12) + .disabled(!itemInteractive) } - .disabled(!itemInteractive) } private var itemInteractive: Bool { @@ -199,17 +205,17 @@ struct CIFileView: View { Image(systemName: icon) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) + .frame(width: smallView ? 36 : 30, height: smallView ? 36 : 30) .foregroundColor(color) if let innerIcon = innerIcon, - let innerIconSize = innerIconSize { + let innerIconSize = innerIconSize, (!smallView || file?.showStatusIconInSmallView == true) { Image(systemName: innerIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(maxHeight: 16) .frame(width: innerIconSize, height: innerIconSize) .foregroundColor(.white) - .padding(.top, 12) + .padding(.top, smallView ? 15 : 12) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 9c12653343..b79f857231 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -15,13 +15,14 @@ struct CIImageView: View { var preview: UIImage? let maxWidth: CGFloat var imgWidth: CGFloat? + var smallView: Bool = false @State private var showFullScreenImage = false var body: some View { let file = chatItem.file VStack(alignment: .center, spacing: 6) { if let uiImage = getLoadedImage(file) { - imageView(uiImage) + Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } } .fullScreenCover(isPresented: $showFullScreenImage) { FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage) } @@ -30,7 +31,7 @@ struct CIImageView: View { showFullScreenImage = false } } else if let preview { - imageView(preview) + Group { if smallView { smallViewImageView(preview) } else { imageView(preview) } } .onTapGesture { if let file = file { switch file.fileStatus { @@ -102,6 +103,23 @@ struct CIImageView: View { } } + private func smallViewImageView(_ img: UIImage) -> some View { + ZStack(alignment: .topTrailing) { + if img.imageData == nil { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + } else { + SwiftyGif(image: img, contentMode: .scaleAspectFill) + .frame(width: maxWidth, height: maxWidth) + } + if chatItem.file?.showStatusIconInSmallView == true { + loadingIndicator() + } + } + } + @ViewBuilder private func loadingIndicator() -> some View { if let file = chatItem.file { switch file.fileStatus { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index c31c4a0da9..3bcd1bde92 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -20,6 +20,7 @@ struct CIVideoView: View { @State private var videoPlaying: Bool = false private let maxWidth: CGFloat private var videoWidth: CGFloat? + private let smallView: Bool @State private var player: AVPlayer? @State private var fullPlayer: AVPlayer? @State private var url: URL? @@ -29,13 +30,15 @@ struct CIVideoView: View { @State private var timeObserver: Any? = nil @State private var fullScreenTimeObserver: Any? = nil @State private var publisher: AnyCancellable? = nil + private var sizeMultiplier: CGFloat { smallView ? 0.38 : 1 } - init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) { + init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false) { self.chatItem = chatItem self.preview = preview self._duration = State(initialValue: duration) self.maxWidth = maxWidth self.videoWidth = videoWidth + self.smallView = smallView if let url = getLoadedVideo(chatItem.file) { let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet() self._urlDecrypted = State(initialValue: decrypted) @@ -49,16 +52,19 @@ struct CIVideoView: View { var body: some View { let file = chatItem.file - ZStack { + ZStack(alignment: smallView ? .topLeading : .center) { ZStack(alignment: .topLeading) { - if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted { + if let file = file, let preview = preview, let decrypted = urlDecrypted, smallView { + smallVideoView(decrypted, file, preview) + } else if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted { videoView(player, decrypted, file, preview, duration) + } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil, smallView { + smallVideoViewEncrypted(file, defaultPreview) } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil { videoViewEncrypted(file, defaultPreview, duration) - } else if let preview { - imageView(preview) - .onTapGesture { - if let file = file { + } else if let preview, let file { + Group { if smallView { smallViewImageView(preview, file) } else { imageView(preview) } } + .onTapGesture { switch file.fileStatus { case .rcvInvitation, .rcvAborted: receiveFileIfValidSize(file: file, receiveFile: receiveFile) @@ -82,16 +88,22 @@ struct CIVideoView: View { default: () } } - } } - durationProgress() + if !smallView { + durationProgress() + } } - if let file = file, showDownloadButton(file.fileStatus) { + if !smallView, let file = file, showDownloadButton(file.fileStatus) { Button { receiveFileIfValidSize(file: file, receiveFile: receiveFile) } label: { playPauseIcon("play.fill") } + } else if let file = file, showDownloadButton(file.fileStatus) && !file.showStatusIconInSmallView { + playPauseIcon("play.fill") + .onTapGesture { + receiveFileIfValidSize(file: file, receiveFile: receiveFile) + } } } } @@ -194,14 +206,63 @@ struct CIVideoView: View { } } + private func smallVideoViewEncrypted(_ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .center) { + ZStack(alignment: .topLeading) { + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) + smallViewImageView(preview, file) + .onTapGesture { + decrypt(file: file) { + showFullScreenPlayer = urlDecrypted != nil + } + } + if file.showStatusIconInSmallView { + // Show nothing + } else if !decryptionInProgress { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + } else { + videoDecryptionProgress() + } + } + if file.showStatusIconInSmallView { + fileStatusIcon() + } + } + } + + private func smallVideoView(_ url: URL, _ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .center) { + ZStack(alignment: .topLeading) { + smallViewImageView(preview, file) + .fullScreenCover(isPresented: $showFullScreenPlayer) { + fullScreenPlayer(url) + } + .onTapGesture { + showFullScreenPlayer = true + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + + if !file.showStatusIconInSmallView { + playPauseIcon("play.fill") + } + } + if file.showStatusIconInSmallView { + fileStatusIcon() + } + } + } + + private func playPauseIcon(_ image: String, _ color: Color = .white) -> some View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier * 1.6 : 12, height: smallView ? 12 * sizeMultiplier * 1.6 : 12) .foregroundColor(color) - .padding(.leading, 4) - .frame(width: 40, height: 40) + .padding(.leading, smallView ? 0 : 4) + .frame(width: 40 * sizeMultiplier, height: 40 * sizeMultiplier) .background(Color.black.opacity(0.35)) .clipShape(Circle()) } @@ -209,9 +270,9 @@ struct CIVideoView: View { private func videoDecryptionProgress(_ color: Color = .white) -> some View { ProgressView() .progressViewStyle(.circular) - .frame(width: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier : 12, height: smallView ? 12 * sizeMultiplier : 12) .tint(color) - .frame(width: 40, height: 40) + .frame(width: smallView ? 40 * sizeMultiplier * 0.9 : 40, height: smallView ? 40 * sizeMultiplier * 0.9 : 40) .background(Color.black.opacity(0.35)) .clipShape(Circle()) } @@ -251,6 +312,15 @@ struct CIVideoView: View { } } + private func smallViewImageView(_ img: UIImage, _ file: CIFile) -> some View { + ZStack(alignment: .topTrailing) { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + } + } + @ViewBuilder private func fileStatusIcon() -> some View { if let file = chatItem.file { switch file.fileStatus { @@ -322,7 +392,7 @@ struct CIVideoView: View { .aspectRatio(contentMode: .fit) .frame(width: size, height: size) .foregroundColor(.white) - .padding(padding) + .padding(smallView ? 0 : padding) } private func progressView() -> some View { @@ -330,7 +400,7 @@ struct CIVideoView: View { .progressViewStyle(.circular) .frame(width: 16, height: 16) .tint(.white) - .padding(11) + .padding(smallView ? 0 : 11) } private func progressCircle(_ progress: Int64, _ total: Int64) -> some View { @@ -342,7 +412,7 @@ struct CIVideoView: View { ) .rotationEffect(.degrees(-90)) .frame(width: 16, height: 16) - .padding([.trailing, .top], 11) + .padding([.trailing, .top], smallView ? 0 : 11) } // TODO encrypt: where file size is checked? diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index ad15d0d342..3df4d5ed83 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -19,11 +19,23 @@ struct CIVoiceView: View { @Binding var playbackState: VoiceMessagePlaybackState @Binding var playbackTime: TimeInterval? @Binding var allowMenu: Bool + var smallView: Bool = false @State private var seek: (TimeInterval) -> Void = { _ in } var body: some View { Group { - if chatItem.chatDir.sent { + if smallView { + VStack (alignment: .leading, spacing: 6) { + HStack { + player() + playerTime() + if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { + playbackSlider() + } + } + .frame(alignment: .leading) + } + } else if chatItem.chatDir.sent { VStack (alignment: .trailing, spacing: 6) { HStack { if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { @@ -317,6 +329,11 @@ struct VoiceMessagePlayer: View { } } +func voiceMessageSizeBasedOnSquareSize(_ squareSize: CGFloat) -> CGFloat { + let squareToCircleRatio = 0.935 + return squareSize + squareSize * (1 - squareToCircleRatio) +} + struct CIVoiceView_Previews: PreviewProvider { static var previews: some View { let sentVoiceMessage: ChatItem = ChatItem( diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index f29e2e14e6..0b9718cf33 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -16,6 +16,11 @@ struct ChatPreviewView: View { @Binding var progressByTimeout: Bool @State var deleting: Bool = false var darkGreen = Color(red: 0, green: 0.5, blue: 0) + @State private var activeVoicePreview: ActiveVoicePreview? = nil + + @State var audioPlayer: AudioPlayer? = nil + @State var playbackState: VoiceMessagePlaybackState = .noPlayback + @State var playbackTime: TimeInterval? = nil @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @@ -43,7 +48,28 @@ struct ChatPreviewView: View { .padding(.horizontal, 8) ZStack(alignment: .topTrailing) { - chatMessagePreview(cItem) + let chat = activeVoicePreview?.chat ?? chat + let ci = activeVoicePreview?.ci ?? chat.chatItems.last + let mc = ci?.content.msgContent + HStack { + if let ci, (showChatPreviews && chatModel.draftChatId != chat.id) || activeVoicePreview != nil { + chatItemContentPreview(chat, ci) + } + let mcIsVoice = switch mc { case .voice: true; default: false } + if !mcIsVoice || mc?.text != "" || chatModel.draftChatId == chat.id { + let xOffset: CGFloat = if case .file = mc { -10 } else { 0 } + ZStack{ + chatMessagePreview(cItem) + } + .offset(x: xOffset) + } + } + .onChange(of: chatModel.stopPreviousRecPlay?.path) { _ in + checkActiveVoice(chat, ci, mc) + } + .onChange(of: activeVoicePreview) { _ in + checkActiveVoice(chat, ci, mc) + } chatStatusImage() .padding(.top, 26) .frame(maxWidth: .infinity, alignment: .trailing) @@ -58,6 +84,21 @@ struct ChatPreviewView: View { .onChange(of: chatModel.deletedChats.contains(chat.chatInfo.id)) { contains in deleting = contains } + + func checkActiveVoice(_ chat: Chat, _ ci: ChatItem?, _ mc: MsgContent?) { + let playing = chatModel.stopPreviousRecPlay + if playing == nil { + activeVoicePreview = nil + } else if activeVoicePreview == nil { + if let ci, let mc, let fileSource = ci.file?.fileSource, case .voice = mc, playing?.path.hasSuffix(fileSource.filePath) == true { + activeVoicePreview = ActiveVoicePreview(chat: chat, ci: ci, mc: mc) + } + } else { + if let playing, let fileSource = ci?.file?.fileSource, !playing.path.hasSuffix(fileSource.filePath) { + activeVoicePreview = nil + } + } + } } @ViewBuilder private func chatPreviewImageOverlayIcon() -> some View { @@ -172,7 +213,7 @@ struct ChatPreviewView: View { func chatItemPreview(_ cItem: ChatItem) -> Text { let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText() let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil - return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) + return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) // same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey; // can be refactored into a single function if functions calling these are changed to return same type @@ -225,6 +266,53 @@ struct ChatPreviewView: View { } } + @ViewBuilder func chatItemContentPreview(_ chat: Chat, _ ci: ChatItem) -> some View { + let mc = ci.content.msgContent + switch mc { + case let .link(_, preview): + smallContentPreview( + ZStack(alignment: .topTrailing) { + Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 36, height: 36) + ZStack { + Image(systemName: "arrow.up.right") + .resizable() + .colorMultiply(Color.white) + .frame(width: 8, height: 8) + } + .frame(width: 16, height: 16) + .background(Color.black.opacity(0.25)) + .cornerRadius(8) + } + .onTapGesture { + UIApplication.shared.open(preview.uri) + } + ) + case let .image(_, image): + smallContentPreview( + CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: 36, smallView: true) + .environmentObject(ReverseListScrollModel()) + ) + case let .video(_,image, duration): + smallContentPreview( + CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: 36, videoWidth: nil, smallView: true) + .environmentObject(ReverseListScrollModel()) + ) + case let .voice(_, duration): + smallContentPreviewVoice( + CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: Binding.constant(true), smallView: true) + ) + case .file: + smallContentPreviewFile( + CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallView: true) + ) + default: EmptyView() + } + } + + @ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View { groupInfo.membership.memberIncognito ? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)") @@ -294,10 +382,50 @@ struct ChatPreviewView: View { } } +func smallContentPreview(_ view: some View) -> some View { + ZStack { + view + .frame(width: 36, height: 36) + } + .cornerRadius(8) + .overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)) + .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) + .padding(.leading, 3) + .offset(x: 6) +} + +func smallContentPreviewVoice(_ view: some View) -> some View { + ZStack { + view + .frame(height: voiceMessageSizeBasedOnSquareSize(36)) + } + .padding(.leading, 3) + .offset(x: 6) +} + +func smallContentPreviewFile(_ view: some View) -> some View { + ZStack { + view + .frame(width: 36, height: 36) + } + .padding(.leading, 5) + .offset(y: -1) +} + func unreadCountText(_ n: Int) -> Text { Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "") } +private struct ActiveVoicePreview: Equatable { + var chat: Chat + var ci: ChatItem + var mc: MsgContent + + static func == (lhs: ActiveVoicePreview, rhs: ActiveVoicePreview) -> Bool { + lhs.chat.id == rhs.chat.id && lhs.ci.id == rhs.ci.id && lhs.mc == rhs.mc + } +} + struct ChatPreviewView_Previews: PreviewProvider { static var previews: some View { Group { diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index d3e61c88f9..0000000000 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,49 +0,0 @@ -{ - "originHash" : "e2611d1e91fd8071abc106776ba14ee2e395d2ad08a78e073381294abc10f115", - "pins" : [ - { - "identity" : "codescanner", - "kind" : "remoteSourceControl", - "location" : "https://github.com/twostraws/CodeScanner", - "state" : { - "revision" : "c27a66149b7483fe42e2ec6aad61d5c3fffe522d", - "version" : "2.1.1" - } - }, - { - "identity" : "lzstring-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Ibrahimhass/lzstring-swift", - "state" : { - "revision" : "7f62f21de5b18582a950e1753b775cc614722407" - } - }, - { - "identity" : "swiftygif", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kirualex/SwiftyGif", - "state" : { - "branch" : "master", - "revision" : "5e8619335d394901379c9add5c4c1c2f420b3800" - } - }, - { - "identity" : "webrtc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/simplex-chat/WebRTC.git", - "state" : { - "revision" : "34bedc50f9c58dccf4967ea59c7e6a47d620803b" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams", - "state" : { - "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", - "version" : "5.1.2" - } - } - ], - "version" : 3 -} diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 7c113e37c3..3898d1e4ea 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3294,6 +3294,28 @@ public struct CIFile: Decodable, Hashable { } } } + + public var showStatusIconInSmallView: Bool { + get { + switch fileStatus { + case .sndStored: fileProtocol != .local + case .sndTransfer: true + case .sndComplete: false + case .sndCancelled: true + case .sndError: true + case .sndWarning: true + case .rcvInvitation: false + case .rcvAccepted: true + case .rcvTransfer: true + case .rcvAborted: true + case .rcvCancelled: true + case .rcvComplete: false + case .rcvError: true + case .rcvWarning: true + case .invalid: true + } + } + } } public struct CryptoFile: Codable, Hashable {