mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-03 15:31:43 +00:00
ios: Animated images (GIF) support (#1636)
* ios: Animated images (GIF) support * Moved from String path to UIImage param * Aspect ratio * Image frame * gif image size * refactor * refactor * fix fullscreen scroll animation * rename UploadContent -> AnyImage * refactor, allow using gifs in profiles * rename back Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6205b03943
commit
cd63f81292
@@ -1006,7 +1006,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.addChatItem(cInfo, cItem)
|
||||
if let file = cItem.file,
|
||||
let mc = cItem.content.msgContent,
|
||||
file.fileSize <= MAX_IMAGE_SIZE {
|
||||
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV {
|
||||
let acceptImages = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
if (mc.isImage && acceptImages)
|
||||
|| (mc.isVoice && ((file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND && acceptImages) || cInfo.chatType == .group)) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Created by Avently on 19.12.2022.
|
||||
// Copyright (c) 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class AnimatedImageView: UIView {
|
||||
var image: UIImage? = nil
|
||||
var imageView: UIImageView? = nil
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
convenience init(image: UIImage) {
|
||||
self.init()
|
||||
self.image = image
|
||||
imageView = UIImageView(gifImage: image)
|
||||
imageView!.contentMode = .scaleAspectFit
|
||||
self.addSubview(imageView!)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
imageView!.frame = bounds
|
||||
}
|
||||
|
||||
func updateImage(_ image: UIImage) {
|
||||
if let subview = self.subviews.first as? UIImageView {
|
||||
if image.imageData != subview.gifImage?.imageData {
|
||||
imageView = UIImageView(gifImage: image)
|
||||
imageView!.contentMode = .scaleAspectFit
|
||||
self.addSubview(imageView!)
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
imageView!.frame = bounds
|
||||
self.layoutSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
struct SwiftyGif: UIViewRepresentable {
|
||||
private let image: UIImage
|
||||
|
||||
init(image: UIImage) {
|
||||
self.image = image
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> AnimatedImageView {
|
||||
AnimatedImageView(image: image)
|
||||
}
|
||||
|
||||
func updateUIView(_ imageView: AnimatedImageView, context: Context) {
|
||||
imageView.updateImage(image)
|
||||
imageView.imageView!.startAnimatingGif()
|
||||
}
|
||||
}
|
||||
@@ -55,13 +55,19 @@ struct CIImageView: View {
|
||||
}
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width > img.size.height ? .infinity : maxWidth * 0.75
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth
|
||||
DispatchQueue.main.async { imgWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: w)
|
||||
if img.imageData == nil {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: w)
|
||||
} else {
|
||||
SwiftyGif(image: img)
|
||||
.frame(width: w, height: w * img.size.height / img.size.width)
|
||||
.scaledToFit()
|
||||
}
|
||||
loadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SwiftyGif
|
||||
|
||||
struct FullScreenImageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@@ -77,9 +78,14 @@ struct FullScreenImageView: View {
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
ZStack {
|
||||
Color.black
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
if img.imageData == nil {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
} else {
|
||||
SwiftyGif(image: img)
|
||||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SwiftyGif
|
||||
|
||||
private let memberImageSize: CGFloat = 34
|
||||
|
||||
@@ -465,13 +466,15 @@ struct ChatView: View {
|
||||
}
|
||||
menu.append(shareUIAction())
|
||||
menu.append(copyUIAction())
|
||||
if let filePath = getLoadedFilePath(ci.file) {
|
||||
if case .image = ci.content.msgContent, let image = UIImage(contentsOfFile: filePath) {
|
||||
menu.append(saveImageAction(image))
|
||||
} else {
|
||||
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
|
||||
if image.imageData != nil, let filePath = getLoadedFilePath(ci.file) {
|
||||
menu.append(saveFileAction(filePath))
|
||||
} else {
|
||||
menu.append(saveImageAction(image))
|
||||
}
|
||||
} else if case .file = ci.content.msgContent, let filePath = getLoadedFilePath(ci.file) {
|
||||
menu.append(saveFileAction(filePath))
|
||||
}
|
||||
}
|
||||
if ci.meta.editable && !mc.isVoice {
|
||||
menu.append(editAction())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SwiftyGif
|
||||
import PhotosUI
|
||||
|
||||
enum ComposePreview {
|
||||
case noPreview
|
||||
@@ -169,6 +171,37 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
||||
return chatItemPreview
|
||||
}
|
||||
|
||||
enum UploadContent: Equatable {
|
||||
case simpleImage(image: UIImage)
|
||||
case animatedImage(image: UIImage)
|
||||
|
||||
var uiImage: UIImage {
|
||||
switch self {
|
||||
case let .simpleImage(image): return image
|
||||
case let .animatedImage(image): return image
|
||||
}
|
||||
}
|
||||
|
||||
static func loadFromURL(url: URL) -> UploadContent? {
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
if let image = UIImage(data: data) {
|
||||
try image.setGifFromData(data, levelOfIntegrity: 1.0)
|
||||
logger.log("UploadContent: added animated image")
|
||||
return .animatedImage(image: image)
|
||||
} else { return nil }
|
||||
} catch {
|
||||
do {
|
||||
if let image = try UIImage(data: Data(contentsOf: url)) {
|
||||
logger.log("UploadContent: added simple image")
|
||||
return .simpleImage(image: image)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@@ -183,7 +216,7 @@ struct ComposeView: View {
|
||||
@State private var showChooseSource = false
|
||||
@State private var showImagePicker = false
|
||||
@State private var showTakePhoto = false
|
||||
@State var chosenImages: [UIImage] = []
|
||||
@State var chosenImages: [UploadContent] = []
|
||||
@State private var showFileImporter = false
|
||||
@State var chosenFile: URL? = nil
|
||||
|
||||
@@ -231,7 +264,7 @@ struct ComposeView: View {
|
||||
},
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
||||
onImageAdded: { image in chosenImages = [image] },
|
||||
onImagesAdded: { images in if !images.isEmpty { chosenImages = images }},
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
@@ -256,7 +289,15 @@ struct ComposeView: View {
|
||||
}
|
||||
if UIPasteboard.general.hasImages {
|
||||
Button("Paste image") {
|
||||
chosenImages = imageList(UIPasteboard.general.image)
|
||||
UIPasteboard.general.itemProviders.forEach { p in
|
||||
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
||||
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
|
||||
if let url = url, let image = UploadContent.loadFromURL(url: url) {
|
||||
chosenImages.append(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Button("Choose file") {
|
||||
@@ -283,7 +324,7 @@ struct ComposeView: View {
|
||||
Task {
|
||||
var imgs: [String] = []
|
||||
for image in images {
|
||||
if let img = resizeImageToStrSize(image, maxDataSize: 14000) {
|
||||
if let img = resizeImageToStrSize(image.uiImage, maxDataSize: 14000) {
|
||||
imgs.append(img)
|
||||
await MainActor.run {
|
||||
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs))
|
||||
@@ -483,12 +524,12 @@ struct ComposeView: View {
|
||||
case let .imagePreviews(imagePreviews: images):
|
||||
let last = min(chosenImages.count, images.count) - 1
|
||||
for i in 0..<last {
|
||||
if let savedFile = saveImage(chosenImages[i]) {
|
||||
if let savedFile = saveAnyImage(chosenImages[i]) {
|
||||
_ = await send(.image(text: "", image: images[i]), quoted: nil, file: savedFile)
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
if let savedFile = saveImage(chosenImages[last]) {
|
||||
if let savedFile = saveAnyImage(chosenImages[last]) {
|
||||
sent = await send(.image(text: msgText, image: images[last]), quoted: quoted, file: savedFile, live: live)
|
||||
}
|
||||
if sent == nil {
|
||||
@@ -585,6 +626,13 @@ struct ComposeView: View {
|
||||
return .text(msgText)
|
||||
}
|
||||
}
|
||||
|
||||
func saveAnyImage(_ img: UploadContent) -> String? {
|
||||
switch img {
|
||||
case let .simpleImage(image): return saveImage(image)
|
||||
case let .animatedImage(image): return saveAnimImage(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startVoiceMessageRecording() async {
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftyGif
|
||||
import SimpleXChat
|
||||
import PhotosUI
|
||||
|
||||
struct NativeTextEditor: UIViewRepresentable {
|
||||
@Binding var text: String
|
||||
@@ -14,7 +17,7 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
let font: UIFont
|
||||
@FocusState.Binding var focused: Bool
|
||||
let alignment: TextAlignment
|
||||
let onImageAdded: (UIImage) -> Void
|
||||
let onImagesAdded: ([UploadContent]) -> Void
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let field = CustomUITextField()
|
||||
@@ -23,10 +26,10 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
field.font = font
|
||||
field.textAlignment = alignment == .leading ? .left : .right
|
||||
field.autocapitalizationType = .sentences
|
||||
field.setOnTextChangedListener { newText, image in
|
||||
field.setOnTextChangedListener { newText, images in
|
||||
text = newText
|
||||
if let image = image {
|
||||
onImageAdded(image)
|
||||
if !images.isEmpty {
|
||||
onImagesAdded(images)
|
||||
}
|
||||
}
|
||||
field.setOnFocusChangedListener { focused = $0 }
|
||||
@@ -43,33 +46,85 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
}
|
||||
|
||||
private class CustomUITextField: UITextView, UITextViewDelegate {
|
||||
var onTextChanged: (String, UIImage?) -> Void = { newText, image in }
|
||||
var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in }
|
||||
var onFocusChanged: (Bool) -> Void = { focused in }
|
||||
|
||||
func setOnTextChangedListener(onTextChanged: @escaping (String, UIImage?) -> Void) {
|
||||
func setOnTextChangedListener(onTextChanged: @escaping (String, [UploadContent]) -> Void) {
|
||||
self.onTextChanged = onTextChanged
|
||||
}
|
||||
|
||||
func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) {
|
||||
self.onFocusChanged = onFocusChanged
|
||||
}
|
||||
|
||||
|
||||
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||
if !UIPasteboard.general.hasImages { return UIMenu(children: suggestedActions)}
|
||||
return UIMenu(children: suggestedActions.map { elem in
|
||||
if let elem = elem as? UIMenu {
|
||||
var actions = elem.children
|
||||
// Replacing Paste action since it allows to paste animated images too
|
||||
let pasteIndex = elem.children.firstIndex { elem in elem.debugDescription.contains("Action: paste:")}
|
||||
if let pasteIndex = pasteIndex {
|
||||
let paste = actions[pasteIndex]
|
||||
actions.remove(at: pasteIndex)
|
||||
let newPaste = UIAction(title: paste.title, image: paste.image) { action in
|
||||
var images: [UploadContent] = []
|
||||
var totalImages = 0
|
||||
var processed = 0
|
||||
UIPasteboard.general.itemProviders.forEach { p in
|
||||
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
||||
totalImages += 1
|
||||
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
|
||||
processed += 1
|
||||
if let url = url, let image = UploadContent.loadFromURL(url: url) {
|
||||
images.append(image)
|
||||
DispatchQueue.main.sync {
|
||||
self.onTextChanged(textView.text, images)
|
||||
}
|
||||
}
|
||||
// No images were added, just paste a text then
|
||||
if processed == totalImages && images.isEmpty {
|
||||
textView.paste(UIPasteboard.general.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actions.insert(newPaste, at: 0)
|
||||
}
|
||||
return UIMenu(title: elem.title, subtitle: elem.subtitle, image: elem.image, identifier: elem.identifier, options: elem.options, children: actions)
|
||||
} else {
|
||||
return elem
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
var image: UIImage? = nil
|
||||
var images: [UploadContent] = []
|
||||
var rangeDiff = 0
|
||||
let newAttributedText = NSMutableAttributedString(attributedString: textView.attributedText)
|
||||
textView.attributedText.enumerateAttribute(
|
||||
NSAttributedString.Key.attachment,
|
||||
in: NSRange(location: 0, length: textView.attributedText.length),
|
||||
options: [],
|
||||
using: { value, range, _ in
|
||||
if let attachment = (value as? NSTextAttachment)?.image {
|
||||
image = attachment
|
||||
let newText = NSMutableAttributedString(attributedString: textView.attributedText)
|
||||
newText.replaceCharacters(in: range, with: "")
|
||||
textView.attributedText = newText
|
||||
if let attachment = (value as? NSTextAttachment)?.fileWrapper?.regularFileContents {
|
||||
do {
|
||||
images.append(.animatedImage(image: try UIImage(gifData: attachment)))
|
||||
} catch {
|
||||
if let img = (value as? NSTextAttachment)?.image {
|
||||
images.append(.simpleImage(image: img))
|
||||
}
|
||||
}
|
||||
newAttributedText.replaceCharacters(in: NSMakeRange(range.location - rangeDiff, range.length), with: "")
|
||||
rangeDiff += range.length
|
||||
}
|
||||
}
|
||||
)
|
||||
onTextChanged(textView.text, image)
|
||||
if textView.attributedText != newAttributedText {
|
||||
textView.attributedText = newAttributedText
|
||||
}
|
||||
onTextChanged(textView.text, images)
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
@@ -90,7 +145,7 @@ struct NativeTextEditor_Previews: PreviewProvider{
|
||||
font: UIFont.preferredFont(forTextStyle: .body),
|
||||
focused: $keyboardVisible,
|
||||
alignment: TextAlignment.leading,
|
||||
onImageAdded: { _ in }
|
||||
onImagesAdded: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ struct SendMessageView: View {
|
||||
var startVoiceMessageRecording: (() -> Void)? = nil
|
||||
var finishVoiceMessageRecording: (() -> Void)? = nil
|
||||
var allowVoiceMessagesToContact: (() -> Void)? = nil
|
||||
var onImageAdded: (UIImage) -> Void
|
||||
var onImagesAdded: ([UploadContent]) -> Void
|
||||
@State private var holdingVMR = false
|
||||
@Namespace var namespace
|
||||
@FocusState.Binding var keyboardVisible: Bool
|
||||
@@ -66,7 +66,7 @@ struct SendMessageView: View {
|
||||
font: teUiFont,
|
||||
focused: $keyboardVisible,
|
||||
alignment: alignment,
|
||||
onImageAdded: onImageAdded
|
||||
onImagesAdded: onImagesAdded
|
||||
)
|
||||
.allowsTightening(false)
|
||||
.frame(height: teHeight)
|
||||
@@ -314,7 +314,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||
SendMessageView(
|
||||
composeState: $composeStateNew,
|
||||
sendMessage: {},
|
||||
onImageAdded: { _ in },
|
||||
onImagesAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
}
|
||||
@@ -324,7 +324,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||
SendMessageView(
|
||||
composeState: $composeStateEditing,
|
||||
sendMessage: {},
|
||||
onImageAdded: { _ in },
|
||||
onImagesAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,28 +8,34 @@
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import SwiftyGif
|
||||
import SimpleXChat
|
||||
|
||||
struct LibraryImagePicker: View {
|
||||
@Binding var image: UIImage?
|
||||
var didFinishPicking: (_ didSelectItems: Bool) -> Void
|
||||
@State var images: [UIImage] = []
|
||||
@State var images: [UploadContent] = []
|
||||
|
||||
var body: some View {
|
||||
LibraryImageListPicker(images: $images, selectionLimit: 1, didFinishPicking: didFinishPicking)
|
||||
.onChange(of: images) { image = $0.first }
|
||||
.onChange(of: images) { _ in
|
||||
if let img = images.first {
|
||||
image = img.uiImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = PHPickerViewController
|
||||
@Binding var images: [UIImage]
|
||||
@Binding var images: [UploadContent]
|
||||
var selectionLimit: Int
|
||||
var didFinishPicking: (_ didSelectItems: Bool) -> Void
|
||||
|
||||
class Coordinator: PHPickerViewControllerDelegate {
|
||||
let parent: LibraryImageListPicker
|
||||
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryImageListPicker")
|
||||
var images: [UIImage] = []
|
||||
var images: [UploadContent] = []
|
||||
var imageCount: Int = 0
|
||||
|
||||
init(_ parent: LibraryImageListPicker) {
|
||||
@@ -48,7 +54,11 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
for result in results {
|
||||
logger.log("LibraryImageListPicker result")
|
||||
let p = result.itemProvider
|
||||
if p.canLoadObject(ofClass: UIImage.self) {
|
||||
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
||||
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
|
||||
self.loadImage(object: url, error: error)
|
||||
}
|
||||
} else if p.canLoadObject(ofClass: UIImage.self) {
|
||||
p.loadObject(ofClass: UIImage.self) { image, error in
|
||||
DispatchQueue.main.async {
|
||||
self.loadImage(object: image, error: error)
|
||||
@@ -72,8 +82,10 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
if let error = error {
|
||||
logger.error("LibraryImageListPicker: couldn't load image with error: \(error.localizedDescription)")
|
||||
} else if let image = object as? UIImage {
|
||||
images.append(image)
|
||||
images.append(.simpleImage(image: image))
|
||||
logger.log("LibraryImageListPicker: added image")
|
||||
} else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) {
|
||||
images.append(image)
|
||||
}
|
||||
dispatchQueue.sync {
|
||||
self.imageCount -= 1
|
||||
@@ -105,20 +117,18 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
struct CameraImageListPicker: View {
|
||||
@Binding var images: [UIImage]
|
||||
@Binding var images: [UploadContent]
|
||||
@State var image: UIImage?
|
||||
|
||||
var body: some View {
|
||||
CameraImagePicker(image: $image)
|
||||
.onChange(of: image) { images = imageList($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func imageList(_ img: UIImage?) -> [UIImage] {
|
||||
if let img = img {
|
||||
return [img]
|
||||
} else {
|
||||
return []
|
||||
.onChange(of: image) { img in
|
||||
if let img = img {
|
||||
images = [UploadContent.simpleImage(image: img)]
|
||||
} else {
|
||||
images = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ struct TerminalView: View {
|
||||
composeState: $composeState,
|
||||
sendMessage: sendMessage,
|
||||
showVoiceMessageButton: false,
|
||||
onImageAdded: { _ in },
|
||||
onImagesAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841511920742C6E152E469F /* AnimatedImageView.swift */; };
|
||||
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C714776281C081000CB4D4B /* WebRTCView.swift */; };
|
||||
3C71477A281C0F6800CB4D4B /* www in Resources */ = {isa = PBXBuildFile; fileRef = 3C714779281C0F6800CB4D4B /* www */; };
|
||||
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; };
|
||||
@@ -157,6 +158,8 @@
|
||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D77B92DE29523E1700A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DD29523E1700A5A1CC /* SwiftyGif */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -216,6 +219,7 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1841511920742C6E152E469F /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = "<group>"; };
|
||||
3C714776281C081000CB4D4B /* WebRTCView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCView.swift; sourceTree = "<group>"; };
|
||||
3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = "<group>"; };
|
||||
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
|
||||
@@ -382,6 +386,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */,
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */,
|
||||
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */,
|
||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */,
|
||||
);
|
||||
@@ -408,6 +413,8 @@
|
||||
files = (
|
||||
5C70311F2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
D77B92DE29523E1700A5A1CC /* SwiftyGif in Frameworks */,
|
||||
5CBE6C2E2948E5B9002D9531 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C70311D2955080A00150A12 /* libgmp.a in Frameworks */,
|
||||
5C70311E2955080A00150A12 /* libffi.a in Frameworks */,
|
||||
@@ -693,6 +700,7 @@
|
||||
5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */,
|
||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */,
|
||||
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */,
|
||||
1841511920742C6E152E469F /* AnimatedImageView.swift */,
|
||||
);
|
||||
path = ChatItem;
|
||||
sourceTree = "<group>";
|
||||
@@ -771,6 +779,7 @@
|
||||
name = "SimpleX (iOS)";
|
||||
packageProductDependencies = (
|
||||
5C8F01CC27A6F0D8007D2C8D /* CodeScanner */,
|
||||
D77B92DB2952372200A5A1CC /* SwiftyGif */,
|
||||
);
|
||||
productName = "SimpleX (iOS)";
|
||||
productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */;
|
||||
@@ -826,6 +835,9 @@
|
||||
dependencies = (
|
||||
);
|
||||
name = SimpleXChat;
|
||||
packageProductDependencies = (
|
||||
D77B92DD29523E1700A5A1CC /* SwiftyGif */,
|
||||
);
|
||||
productName = SimpleXChat;
|
||||
productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
@@ -872,6 +884,7 @@
|
||||
mainGroup = 5CA059BD279559F40002BEB4;
|
||||
packageReferences = (
|
||||
5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */,
|
||||
D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */,
|
||||
);
|
||||
productRefGroup = 5CA059CB279559F40002BEB4 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -1040,6 +1053,7 @@
|
||||
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
|
||||
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
|
||||
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */,
|
||||
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */,
|
||||
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1602,6 +1616,14 @@
|
||||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kirualex/SwiftyGif";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -1610,6 +1632,16 @@
|
||||
package = 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */;
|
||||
productName = CodeScanner;
|
||||
};
|
||||
D77B92DB2952372200A5A1CC /* SwiftyGif */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */;
|
||||
productName = SwiftyGif;
|
||||
};
|
||||
D77B92DD29523E1700A5A1CC /* SwiftyGif */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */;
|
||||
productName = SwiftyGif;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 5CA059BE279559F40002BEB4 /* Project object */;
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
"revision" : "c27a66149b7483fe42e2ec6aad61d5c3fffe522d",
|
||||
"version" : "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "4a6f5bad863c5365b192f8441f62c713ecff62bd"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftyGif
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
public let MAX_IMAGE_SIZE: Int64 = 236700
|
||||
|
||||
public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2
|
||||
|
||||
public let MAX_FILE_SIZE: Int64 = 8000000
|
||||
|
||||
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(30)
|
||||
@@ -182,8 +185,17 @@ public func getLoadedFilePath(_ file: CIFile?) -> String? {
|
||||
}
|
||||
|
||||
public func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
||||
if let filePath = getLoadedFilePath(file) {
|
||||
return UIImage(contentsOfFile: filePath)
|
||||
let loadedFilePath = getLoadedFilePath(file)
|
||||
if let loadedFilePath = loadedFilePath, let fileName = file?.filePath {
|
||||
let filePath = getAppFilePath(fileName)
|
||||
do {
|
||||
let data = try Data(contentsOf: filePath)
|
||||
let img = UIImage(data: data)
|
||||
try img?.setGifFromData(data, levelOfIntegrity: 1.0)
|
||||
return img
|
||||
} catch {
|
||||
return UIImage(contentsOfFile: loadedFilePath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -216,6 +228,12 @@ public func saveImage(_ uiImage: UIImage) -> String? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func saveAnimImage(_ image: UIImage) -> String? {
|
||||
let fileName = generateNewFileName("IMG", "gif")
|
||||
guard let imageData = image.imageData else { return nil }
|
||||
return saveFile(imageData, fileName)
|
||||
}
|
||||
|
||||
public func generateNewFileName(_ prefix: String, _ ext: String) -> String {
|
||||
let timestamp = Date().getFormattedDate("yyyyMMdd_HHmmss")
|
||||
let fileName = uniqueCombine("\(prefix)_\(timestamp).\(ext)")
|
||||
|
||||
Reference in New Issue
Block a user