From 58cc4dbd5bd60a222726a30965ea199fb50350f9 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Sun, 21 Jul 2024 07:03:52 -0700 Subject: [PATCH] voice message support and various fixes --- .../Views/Chat/ChatItem/CIImageView.swift | 5 +- .../Views/Chat/ChatItem/CIVideoView.swift | 12 +- .../Views/Chat/ChatItem/CIVoiceView.swift | 149 ++++++++++++------ .../Chat/ChatItem/FramedCIVoiceView.swift | 3 +- .../Views/Chat/ChatItem/FramedItemView.swift | 5 +- .../Views/ChatList/ChatPreviewView.swift | 140 +++++++++------- .../xcshareddata/swiftpm/Package.resolved | 49 ++++++ 7 files changed, 253 insertions(+), 110 deletions(-) create mode 100644 apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index b79f857231..b247a33565 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -16,7 +16,7 @@ struct CIImageView: View { let maxWidth: CGFloat var imgWidth: CGFloat? var smallView: Bool = false - @State private var showFullScreenImage = false + @Binding var showFullScreenImage: Bool var body: some View { let file = chatItem.file @@ -84,6 +84,9 @@ struct CIImageView: View { } } } + .onDisappear { + showFullScreenImage = false + } } private func imageView(_ img: UIImage) -> some View { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index 3bcd1bde92..e48c6e4dfd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -26,19 +26,20 @@ struct CIVideoView: View { @State private var url: URL? @State private var urlDecrypted: URL? @State private var decryptionInProgress: Bool = false - @State private var showFullScreenPlayer = false + @Binding private var showFullScreenPlayer: Bool @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?, smallView: Bool = false) { + init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false, showFullscreenPlayer: Binding) { self.chatItem = chatItem self.preview = preview self._duration = State(initialValue: duration) self.maxWidth = maxWidth self.videoWidth = videoWidth self.smallView = smallView + self._showFullScreenPlayer = showFullscreenPlayer if let url = getLoadedVideo(chatItem.file) { let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet() self._urlDecrypted = State(initialValue: decrypted) @@ -106,6 +107,9 @@ struct CIVideoView: View { } } } + .onDisappear { + showFullScreenPlayer = false + } } private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool { @@ -226,6 +230,8 @@ struct CIVideoView: View { } if file.showStatusIconInSmallView { fileStatusIcon() + // prevent stealing touches and opening chat view + .allowsHitTesting(false) } } } @@ -250,6 +256,8 @@ struct CIVideoView: View { } if file.showStatusIconInSmallView { fileStatusIcon() + // prevent stealing touches and opening chat view + .allowsHitTesting(false) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 3df4d5ed83..c9d5b4bf71 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -25,15 +25,13 @@ struct CIVoiceView: View { var body: some View { Group { if smallView { - VStack (alignment: .leading, spacing: 6) { - HStack { - player() - playerTime() - if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { - playbackSlider() - } + HStack(spacing: 10) { + player() + playerTime() + .allowsHitTesting(false) + if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { + playbackSlider() } - .frame(alignment: .leading) } } else if chatItem.chatDir.sent { VStack (alignment: .trailing, spacing: 6) { @@ -75,7 +73,8 @@ struct CIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: smallView ? voiceMessageSizeBasedOnSquareSize(36) / 56 : 1 ) } @@ -143,6 +142,7 @@ struct VoiceMessagePlayer: View { @Binding var playbackState: VoiceMessagePlaybackState @Binding var playbackTime: TimeInterval? @Binding var allowMenu: Bool + var sizeMultiplier: CGFloat var body: some View { ZStack { @@ -212,39 +212,81 @@ struct VoiceMessagePlayer: View { .onChange(of: chatModel.stopPreviousRecPlay) { it in if let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) { - audioPlayer?.stop() - playbackState = .noPlayback - playbackTime = TimeInterval(0) + stopPlayback() } } .onChange(of: playbackState) { state in allowMenu = state == .paused || state == .noPlayback + // Notify activeContentPreview in ChatPreviewView that playback is finished + if state == .noPlayback, let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, + chatModel.stopPreviousRecPlay == getAppFilePath(recordingFileName) { + chatModel.stopPreviousRecPlay = nil + } + } +// .onChange(of: chatModel.chatId) { _ in +// if sizeMultiplier != 1 { +// stopPlayback() +// } +// } +// .onChange(of: chatModel.currentUser?.userId) { _ in +// if sizeMultiplier != 1 { +// stopPlayback() +// } +// } + .onDisappear { + if sizeMultiplier != 1 { + stopPlayback() + } } } @ViewBuilder private func playbackButton() -> some View { - switch playbackState { - case .noPlayback: - Button { - if let recordingSource = getLoadedFileSource(recordingFile) { - startPlayback(recordingSource) - } - } label: { + if sizeMultiplier != 1 { + switch playbackState { + case .noPlayback: playPauseIcon("play.fill", theme.colors.primary) - } - case .playing: - Button { - audioPlayer?.pause() - playbackState = .paused - } label: { + .onTapGesture { + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) + } + } + case .playing: playPauseIcon("pause.fill", theme.colors.primary) - } - case .paused: - Button { - audioPlayer?.play() - playbackState = .playing - } label: { + .onTapGesture { + audioPlayer?.pause() + playbackState = .paused + } + case .paused: playPauseIcon("play.fill", theme.colors.primary) + .onTapGesture { + audioPlayer?.play() + playbackState = .playing + } + } + } else { + switch playbackState { + case .noPlayback: + Button { + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) + } + } label: { + playPauseIcon("play.fill", theme.colors.primary) + } + case .playing: + Button { + audioPlayer?.pause() + playbackState = .paused + } label: { + playPauseIcon("pause.fill", theme.colors.primary) + } + case .paused: + Button { + audioPlayer?.play() + playbackState = .playing + } label: { + playPauseIcon("play.fill", theme.colors.primary) + } } } } @@ -254,28 +296,41 @@ struct VoiceMessagePlayer: View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) + .frame(width: 20 * sizeMultiplier, height: 20 * sizeMultiplier) .foregroundColor(color) .padding(.leading, image == "play.fill" ? 4 : 0) - .frame(width: 56, height: 56) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) if recordingTime > 0 { ProgressCircle(length: recordingTime, progress: $playbackTime) - .frame(width: 53, height: 53) // this + ProgressCircle lineWidth = background circle diameter + .frame(width: 53 * sizeMultiplier, height: 53 * sizeMultiplier) // this + ProgressCircle lineWidth = background circle diameter } } } private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View { - Button { - Task { - if let user = chatModel.currentUser { - await receiveFile(user: user, fileId: recordingFile.fileId) + Group { + if sizeMultiplier != 1 { + playPauseIcon(icon, theme.colors.primary) + .onTapGesture { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) + } + } + } + } else { + Button { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) + } + } + } label: { + playPauseIcon(icon, theme.colors.primary) } } - } label: { - playPauseIcon(icon, theme.colors.primary) } } @@ -300,17 +355,17 @@ struct VoiceMessagePlayer: View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: size, height: size) + .frame(width: size * sizeMultiplier, height: size * sizeMultiplier) .foregroundColor(Color(uiColor: .tertiaryLabel)) - .frame(width: 56, height: 56) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } private func loadingIcon() -> some View { ProgressView() - .frame(width: 30, height: 30) - .frame(width: 56, height: 56) + .frame(width: 30 * sizeMultiplier, height: 30 * sizeMultiplier) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } @@ -327,6 +382,12 @@ struct VoiceMessagePlayer: View { audioPlayer?.start(fileSource: recordingSource, at: playbackTime) playbackState = .playing } + + private func stopPlayback() { + audioPlayer?.stop() + playbackState = .noPlayback + playbackTime = TimeInterval(0) + } } func voiceMessageSizeBasedOnSquareSize(_ squareSize: CGFloat) -> CGFloat { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index 59fabb3901..268babb8d3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -36,7 +36,8 @@ struct FramedCIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: 1 ) VoiceMessagePlayerTime( recordingTime: TimeInterval(duration), diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 595d9bf2fc..8277a0b3e4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -26,6 +26,7 @@ struct FramedItemView: View { @Binding var allowMenu: Bool @State private var showSecrets = false @State private var showQuoteSecrets = false + @State private var showFullscreenGallery: Bool = false @Binding var audioPlayer: AudioPlayer? @Binding var playbackState: VoiceMessagePlaybackState @@ -106,7 +107,7 @@ struct FramedItemView: View { } else { switch (chatItem.content.msgContent) { case let .image(text, _): - CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth) + CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear @@ -121,7 +122,7 @@ struct FramedItemView: View { ciMsgContentView(chatItem) } case let .video(text, _, duration): - CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth) + CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth, showFullscreenPlayer: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 82161113cf..36f81696b3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -16,12 +16,14 @@ 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 private var activeContentPreview: ActiveContentPreview? = nil @State var audioPlayer: AudioPlayer? = nil @State var playbackState: VoiceMessagePlaybackState = .noPlayback @State var playbackTime: TimeInterval? = nil + @State private var showFullscreenGallery: Bool = false + @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true var body: some View { @@ -48,32 +50,36 @@ struct ChatPreviewView: View { .padding(.horizontal, 8) ZStack(alignment: .topTrailing) { - let chat = activeVoicePreview?.chat ?? chat - let ci = activeVoicePreview?.ci ?? chat.chatItems.last + let chat = activeContentPreview?.chat ?? chat + let ci = activeContentPreview?.ci ?? chat.chatItems.last let mc = ci?.content.msgContent - HStack { - if let ci, (showChatPreviews && chatModel.draftChatId != chat.id) || activeVoicePreview != nil { + HStack(alignment: .top) { + if let ci, (showChatPreviews && chatModel.draftChatId != chat.id) || activeContentPreview != 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) + let hasFilePreview = if case .file = mc { true } else { false } + chatMessagePreview(cItem, hasFilePreview) + } else { + Spacer() + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) } } .onChange(of: chatModel.stopPreviousRecPlay?.path) { _ in - checkActiveVoice(chat, ci, mc) + checkActiveContentPreview(chat, ci, mc) } - .onChange(of: activeVoicePreview) { _ in - checkActiveVoice(chat, ci, mc) + .onChange(of: activeContentPreview) { _ in + checkActiveContentPreview(chat, ci, mc) + } + .onChange(of: showFullscreenGallery) { _ in + checkActiveContentPreview(chat, ci, mc) } chatStatusImage() .padding(.top, 26) .frame(maxWidth: .infinity, alignment: .trailing) } + .frame(maxWidth: .infinity, alignment: .leading) .padding(.trailing, 8) Spacer() @@ -85,18 +91,26 @@ struct ChatPreviewView: View { deleting = contains } - func checkActiveVoice(_ chat: Chat, _ ci: ChatItem?, _ mc: MsgContent?) { + func checkActiveContentPreview(_ 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) + if case .voice = activeContentPreview?.mc, playing == nil { + activeContentPreview = nil + } else if activeContentPreview == nil { + if case .image = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) } - } else { + if case .video = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + if case .voice = mc, let ci, let mc, let fileSource = ci.file?.fileSource, playing?.path.hasSuffix(fileSource.filePath) == true { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + } else if case .voice = activeContentPreview?.mc { if let playing, let fileSource = ci?.file?.fileSource, !playing.path.hasSuffix(fileSource.filePath) { - activeVoicePreview = nil + activeContentPreview = nil } + } else if !showFullscreenGallery { + activeContentPreview = nil } } } @@ -154,39 +168,44 @@ struct ChatPreviewView: View { .kerning(-2) } - private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { + private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> some View { ZStack(alignment: .topTrailing) { let t = text .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .topLeading) - .padding(.leading, 8) - .padding(.trailing, 36) + .padding(.leading, hasFilePreview ? 0 : 8) + .padding(.trailing, hasFilePreview ? 38 : 36) + .offset(x: hasFilePreview ? -2 : 0) if !showChatPreviews && !draft { t.privacySensitive(true).redacted(reason: .privacy) } else { t } - let s = chat.chatStats - if s.unreadCount > 0 || s.unreadChat { - unreadCountText(s.unreadCount) - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 4) - .frame(minWidth: 18, minHeight: 18) - .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) - .cornerRadius(10) - } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { - Image(systemName: "speaker.slash.fill") - .foregroundColor(theme.colors.secondary) - } else if chat.chatInfo.chatSettings?.favorite ?? false { - Image(systemName: "star.fill") - .resizable() - .scaledToFill() - .frame(width: 18, height: 18) - .padding(.trailing, 1) - .foregroundColor(.secondary.opacity(0.65)) - } + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) + } + } + + @ViewBuilder private func chatInfoIcon(_ chat: Chat) -> some View { + let s = chat.chatStats + if s.unreadCount > 0 || s.unreadChat { + unreadCountText(s.unreadCount) + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 4) + .frame(minWidth: 18, minHeight: 18) + .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) + .cornerRadius(10) + } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { + Image(systemName: "speaker.slash.fill") + .foregroundColor(theme.colors.secondary) + } else if chat.chatInfo.chatSettings?.favorite ?? false { + Image(systemName: "star.fill") + .resizable() + .scaledToFill() + .frame(width: 18, height: 18) + .padding(.trailing, 1) + .foregroundColor(.secondary.opacity(0.65)) } } @@ -237,11 +256,11 @@ struct ChatPreviewView: View { } } - @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View { + @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View { if chatModel.draftChatId == chat.id, let draft = chatModel.draft { - chatPreviewLayout(messageDraft(draft), draft: true) + chatPreviewLayout(messageDraft(draft), draft: true, hasFilePreview) } else if let cItem = cItem { - chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem)) + chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview) } else { switch (chat.chatInfo) { case let .direct(contact): @@ -279,7 +298,8 @@ struct ChatPreviewView: View { ZStack { Image(systemName: "arrow.up.right") .resizable() - .colorMultiply(Color.white) + .foregroundColor(Color.white) + .font(.system(size: 15, weight: .black)) .frame(width: 8, height: 8) } .frame(width: 16, height: 16) @@ -292,18 +312,18 @@ struct ChatPreviewView: View { ) case let .image(_, image): smallContentPreview( - CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: 36, smallView: true) + CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: 36, smallView: true, showFullScreenImage: $showFullscreenGallery) .environmentObject(ReverseListScrollModel()) ) case let .video(_,image, duration): smallContentPreview( - CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: 36, videoWidth: nil, smallView: true) + CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: 36, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery) .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 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) @@ -390,7 +410,7 @@ func smallContentPreview(_ view: some View) -> some View { .cornerRadius(8) .overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)) .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) - .padding(.leading, 3) + .padding([.top, .leading], 3) .offset(x: 6) } @@ -399,8 +419,8 @@ func smallContentPreviewVoice(_ view: some View) -> some View { view .frame(height: voiceMessageSizeBasedOnSquareSize(36)) } - .padding(.leading, 3) - .offset(x: 6) + .padding(.leading, 8) + .padding(.top, 6) } func smallContentPreviewFile(_ view: some View) -> some View { @@ -408,20 +428,20 @@ func smallContentPreviewFile(_ view: some View) -> some View { view .frame(width: 36, height: 36) } + .padding(.top, 2) .padding(.leading, 5) - .offset(y: -1) } func unreadCountText(_ n: Int) -> Text { Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "") } -private struct ActiveVoicePreview: Equatable { +private struct ActiveContentPreview: Equatable { var chat: Chat var ci: ChatItem var mc: MsgContent - static func == (lhs: ActiveVoicePreview, rhs: ActiveVoicePreview) -> Bool { + static func == (lhs: ActiveContentPreview, rhs: ActiveContentPreview) -> Bool { lhs.chat.id == rhs.chat.id && lhs.ci.id == rhs.ci.id && lhs.mc == rhs.mc } } diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..d3e61c88f9 --- /dev/null +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,49 @@ +{ + "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 +}