Removed YYImage and libWebP, fixed a couple of bugs

• Removed YYImage and libWebP dependencies (replaced with custom `AnimatedImageView` class)
• Fixed an issue where animated images (WebP/GIF) may not correctly render in the "All Media" grid UI
• Fixed an issue where selecting a GIF for the display picture would incorrectly convert it into a JPG instead of keeping the GIF
pull/1061/head
Morgan Pretty 1 week ago
parent fab3b5683f
commit 417976995b

@ -440,10 +440,6 @@
FD0E353C2AB9880B006A81F7 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0E353A2AB98773006A81F7 /* AppVersion.swift */; };
FD10AF0C2AF32B9A007709E5 /* SessionListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */; };
FD10AF122AF85D11007709E5 /* Feature+ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */; };
FD11E2292CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A395B2C2D10C700762359 /* YYImage */; };
FD11E22A2CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A394E2C2D060C00762359 /* YYImage */; };
FD11E22B2CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A395B2C2D10C700762359 /* YYImage */; };
FD11E22C2CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39682C2D283A00762359 /* YYImage */; };
FD11E22D2CA4D12C001BAF58 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286782C38D4FF00BC06F7 /* DifferenceKit */; };
FD11E22E2CA4D12C001BAF58 /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = FDEF57292C3CF50B00131302 /* WebRTC */; };
FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */; };
@ -724,10 +720,6 @@
FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393A2C2AD3A300762359 /* Nimble */; };
FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393C2C2AD3AC00762359 /* Nimble */; };
FD6A39412C2AD3B600762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39402C2AD3B600762359 /* Nimble */; };
FD6A39662C2D21E400762359 /* libwebp in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39652C2D21E400762359 /* libwebp */; };
FD6A396B2C2D284500762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A396A2C2D284500762359 /* YYImage */; };
FD6A396D2C2D284B00762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A396C2C2D284B00762359 /* YYImage */; };
FD6A396F2C2E3D4400762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A396E2C2E3D4400762359 /* YYImage */; };
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; };
FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; };
FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; };
@ -839,6 +831,7 @@
FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; };
FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; };
FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; };
FDA335F52D91157A007E0EB6 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA335F42D911576007E0EB6 /* AnimatedImageView.swift */; };
FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */; };
FDAA167B2AC28E2F00DDBF77 /* SnodeRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */; };
FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */; };
@ -2018,6 +2011,7 @@
FD99D0912D10F5EB005D2E15 /* ThreadSafeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeSpec.swift; sourceTree = "<group>"; };
FD9AECA42AAA9609009B3406 /* NotificationResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationResolution.swift; sourceTree = "<group>"; };
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; };
FDA335F42D911576007E0EB6 /* AnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = "<group>"; };
FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsType.swift; sourceTree = "<group>"; };
FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeRequestSpec.swift; sourceTree = "<group>"; };
FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Sound.swift"; sourceTree = "<group>"; };
@ -2286,7 +2280,6 @@
buildActionMask = 2147483647;
files = (
FD756BF22D06687800BD7199 /* Lucide in Frameworks */,
FD6A396B2C2D284500762359 /* YYImage in Frameworks */,
FD2286712C38D43000BC06F7 /* DifferenceKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -2295,7 +2288,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FD6A396F2C2E3D4400762359 /* YYImage in Frameworks */,
C38EF48A255B7E3F007E1867 /* SessionUIKit.framework in Frameworks */,
FD6A39222C2AA91D00762359 /* NVActivityIndicatorView in Frameworks */,
FD22866F2C38D42300BC06F7 /* DifferenceKit in Frameworks */,
@ -2320,10 +2312,8 @@
buildActionMask = 2147483647;
files = (
FD6673F62D7021E700041530 /* SessionUtil in Frameworks */,
FD6A39662C2D21E400762359 /* libwebp in Frameworks */,
FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */,
FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */,
FD6A396D2C2D284B00762359 /* YYImage in Frameworks */,
FD756BEB2D0181D700BD7199 /* GRDB in Frameworks */,
FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */,
);
@ -2348,10 +2338,6 @@
files = (
FD11E22E2CA4D12C001BAF58 /* WebRTC in Frameworks */,
FD11E22D2CA4D12C001BAF58 /* DifferenceKit in Frameworks */,
FD11E22C2CA4D12C001BAF58 /* YYImage in Frameworks */,
FD11E22B2CA4D12C001BAF58 /* YYImage in Frameworks */,
FD11E22A2CA4D12C001BAF58 /* YYImage in Frameworks */,
FD11E2292CA4D12C001BAF58 /* YYImage in Frameworks */,
B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */,
B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */,
FDB6A87C2AD75B7F002D4F96 /* PhotosUI.framework in Frameworks */,
@ -3101,6 +3087,7 @@
isa = PBXGroup;
children = (
942256932C23F8DD00C0FDBF /* SwiftUI */,
FDA335F42D911576007E0EB6 /* AnimatedImageView.swift */,
B8B5BCEB2394D869003823C9 /* SessionButton.swift */,
FD52090228B4680F006098F6 /* RadioButton.swift */,
B8BB82B02390C37000BA5194 /* SearchBar.swift */,
@ -4869,7 +4856,6 @@
);
name = SessionUIKit;
packageProductDependencies = (
FD6A396A2C2D284500762359 /* YYImage */,
FD2286702C38D43000BC06F7 /* DifferenceKit */,
FD756BF12D06687800BD7199 /* Lucide */,
);
@ -4893,7 +4879,6 @@
name = SignalUtilitiesKit;
packageProductDependencies = (
FD6A39212C2AA91D00762359 /* NVActivityIndicatorView */,
FD6A396E2C2E3D4400762359 /* YYImage */,
FD22866E2C38D42300BC06F7 /* DifferenceKit */,
);
productName = SignalUtilitiesKit;
@ -4945,8 +4930,6 @@
FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */,
FD6A38EB2C2A63B500762359 /* KeychainSwift */,
FD6A38EE2C2A641200762359 /* DifferenceKit */,
FD6A39652C2D21E400762359 /* libwebp */,
FD6A396C2C2D284B00762359 /* YYImage */,
FD756BEA2D0181D700BD7199 /* GRDB */,
FD6673F52D7021E700041530 /* SessionUtil */,
);
@ -5008,10 +4991,6 @@
);
name = Session;
packageProductDependencies = (
FD6A395B2C2D10C700762359 /* YYImage */,
FD6A394E2C2D060C00762359 /* YYImage */,
FD6A395B2C2D10C700762359 /* YYImage */,
FD6A39682C2D283A00762359 /* YYImage */,
FD2286782C38D4FF00BC06F7 /* DifferenceKit */,
FDEF57292C3CF50B00131302 /* WebRTC */,
FD6DA9CE2D015B440092085A /* Lucide */,
@ -5238,8 +5217,6 @@
FD6A39202C2AA91D00762359 /* XCRemoteSwiftPackageReference "NVActivityIndicatorView" */,
FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */,
FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */,
FD6A39642C2D21E400762359 /* XCRemoteSwiftPackageReference "libwebp-Xcode" */,
FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */,
FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */,
FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */,
946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */,
@ -5801,6 +5778,7 @@
C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */,
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */,
94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */,
FDA335F52D91157A007E0EB6 /* AnimatedImageView.swift in Sources */,
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */,
7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */,
@ -10244,22 +10222,6 @@
version = 13.3.0;
};
};
FD6A39642C2D21E400762359 /* XCRemoteSwiftPackageReference "libwebp-Xcode" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SDWebImage/libwebp-Xcode.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.2;
};
};
FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/session-foundation/session-ios-yyimage";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.0;
};
};
FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/session-foundation/session-grdb-swift.git";
@ -10389,39 +10351,6 @@
package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */;
productName = Nimble;
};
FD6A394E2C2D060C00762359 /* YYImage */ = {
isa = XCSwiftPackageProductDependency;
productName = YYImage;
};
FD6A395B2C2D10C700762359 /* YYImage */ = {
isa = XCSwiftPackageProductDependency;
productName = YYImage;
};
FD6A39652C2D21E400762359 /* libwebp */ = {
isa = XCSwiftPackageProductDependency;
package = FD6A39642C2D21E400762359 /* XCRemoteSwiftPackageReference "libwebp-Xcode" */;
productName = libwebp;
};
FD6A39682C2D283A00762359 /* YYImage */ = {
isa = XCSwiftPackageProductDependency;
package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */;
productName = YYImage;
};
FD6A396A2C2D284500762359 /* YYImage */ = {
isa = XCSwiftPackageProductDependency;
package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */;
productName = YYImage;
};
FD6A396C2C2D284B00762359 /* YYImage */ = {
isa = XCSwiftPackageProductDependency;
package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */;
productName = YYImage;
};
FD6A396E2C2E3D4400762359 /* YYImage */ = {
isa = XCSwiftPackageProductDependency;
package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */;
productName = YYImage;
};
FD6DA9CE2D015B440092085A /* Lucide */ = {
isa = XCSwiftPackageProductDependency;
productName = Lucide;

@ -1,5 +1,5 @@
{
"originHash" : "e3fdf2f44acd1f05dab295d0c9e3faf05f5e4461d512be1d5a77af42e0a25e48",
"originHash" : "3976430cfdaea7445596ad6123334158bdc83e4997da535d15a15afc3c7aa091",
"pins" : [
{
"identity" : "cocoalumberjack",
@ -55,15 +55,6 @@
"version" : "1.2.1"
}
},
{
"identity" : "libwebp-xcode",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/libwebp-Xcode.git",
"state" : {
"revision" : "0d60654eeefd5d7d2bef3835804892c40225e8b2",
"version" : "1.5.0"
}
},
{
"identity" : "nimble",
"kind" : "remoteSourceControl",
@ -109,15 +100,6 @@
"version" : "107.3.0"
}
},
{
"identity" : "session-ios-yyimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/session-foundation/session-ios-yyimage",
"state" : {
"revision" : "14786afd2523f80be304b377f9dbab6b7904bf02",
"version" : "1.1.0"
}
},
{
"identity" : "session-lucide",
"kind" : "remoteSourceControl",

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import YYImage
import Combine
import CallKit
import GRDB
@ -31,7 +30,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
let contactName: String
let profilePicture: UIImage
let animatedProfilePicture: YYImage?
let animatedProfilePictureData: Data?
// MARK: - Control
@ -167,10 +166,10 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
self.profilePicture = avatarData
.map { UIImage(data: $0) }
.defaulting(to: PlaceholderIcon.generate(seed: sessionId, text: self.contactName, size: 300))
self.animatedProfilePicture = avatarData
.map { data -> YYImage? in
self.animatedProfilePictureData = avatarData
.map { data -> Data? in
switch data.guessedImageFormat {
case .gif, .webp: return YYImage(data: data)
case .gif, .webp: return data
default: return nil
}
}

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import YYImage
import MediaPlayer
import SessionUIKit
import SessionMessagingKit
@ -141,15 +140,15 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
return result
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
result.image = self.call.animatedProfilePicture
private lazy var animatedImageView: AnimatedImageView = {
let result: AnimatedImageView = AnimatedImageView()
result.loadAnimatedImage(from: self.call.animatedProfilePictureData)
result.set(.width, to: CallVC.avatarRadius * 2)
result.set(.height, to: CallVC.avatarRadius * 2)
result.layer.cornerRadius = CallVC.avatarRadius
result.layer.masksToBounds = true
result.contentMode = .scaleAspectFill
result.isHidden = (self.call.animatedProfilePicture == nil)
result.isHidden = (self.call.animatedProfilePictureData == nil)
return result
}()

@ -3,7 +3,6 @@
import Foundation
import Combine
import GRDB
import YYImage
import DifferenceKit
import SessionUIKit
import SessionSnodeKit

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import YYImage
import SessionUIKit
import SessionMessagingKit
import SignalUtilitiesKit
@ -150,7 +149,7 @@ public class MediaView: UIView {
}
private func configureForAnimatedImage(attachment: Attachment) {
let animatedImageView: YYAnimatedImageView = YYAnimatedImageView()
let animatedImageView: AnimatedImageView = AnimatedImageView()
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
animatedImageView.contentMode = MediaView.contentMode
@ -183,18 +182,19 @@ public class MediaView: UIView {
return
}
applyMediaBlock(YYImage(contentsOfFile: filePath))
applyMediaBlock(filePath as AnyObject)
},
applyMediaBlock: { media in
applyMediaBlock: { filePath in
Log.assertOnMainThread()
guard let image: YYImage = media as? YYImage else {
Log.error("[MediaView] Media has unexpected type: \(type(of: media))")
guard let filePath: String = filePath as? String else {
Log.error("[MediaView] Media has unexpected type: \(type(of: filePath))")
self?.configure(forError: .invalid)
return
}
// FIXME: Animated images flicker when reloading the cells (even though they are in the cache)
animatedImageView.image = image
animatedImageView.loadAnimatedImage(from: filePath)
},
cacheKey: attachment.id
)

@ -1,13 +1,12 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import YYImage
import SessionUIKit
/// Shown when the user taps a profile picture in the conversation settings.
final class ProfilePictureVC: BaseVC {
private let image: UIImage?
private let animatedImage: YYImage?
private let animatedImageData: Data?
private let snTitle: String
private var imageSize: CGFloat { (UIScreen.main.bounds.width - (2 * Values.largeSpacing)) }
@ -21,7 +20,7 @@ final class ProfilePictureVC: BaseVC {
result.layer.cornerRadius = (imageSize / 2)
result.isHidden = (
image != nil ||
animatedImage != nil
animatedImageData != nil
)
result.set(.width, to: imageSize)
result.set(.height, to: imageSize)
@ -41,12 +40,13 @@ final class ProfilePictureVC: BaseVC {
return result
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView(image: animatedImage)
private lazy var animatedImageView: AnimatedImageView = {
let result: AnimatedImageView = AnimatedImageView()
result.loadAnimatedImage(from: animatedImageData)
result.clipsToBounds = true
result.contentMode = .scaleAspectFill
result.layer.cornerRadius = (imageSize / 2)
result.isHidden = (animatedImage == nil)
result.isHidden = (animatedImageData == nil)
result.set(.width, to: imageSize)
result.set(.height, to: imageSize)
@ -55,9 +55,9 @@ final class ProfilePictureVC: BaseVC {
// MARK: - Initialization
init(image: UIImage?, animatedImage: YYImage?, title: String) {
init(image: UIImage?, animatedImageData: Data?, title: String) {
self.image = image
self.animatedImage = animatedImage
self.animatedImageData = animatedImageData
self.snTitle = title
super.init(nibName: nil, bundle: nil)

@ -4,7 +4,6 @@ import Foundation
import Combine
import Lucide
import GRDB
import YYImage
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
@ -823,9 +822,9 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob
nil :
UIImage(data: displayPictureData)
),
animatedImage: (format != .gif && format != .webp ?
animatedImageData: (format != .gif && format != .webp ?
nil :
YYImage(data: displayPictureData)
displayPictureData
),
title: threadViewModel.displayName
)

@ -1,9 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import UIKit
import Combine
import UniformTypeIdentifiers
import YYImage
import SessionUIKit
import SessionSnodeKit
import SignalUtilitiesKit
import SessionUtilitiesKit
@ -37,7 +37,7 @@ class GifPickerCell: UICollectionViewCell {
var stillAsset: ProxiedContentAsset?
var animatedAssetRequest: ProxiedContentAssetRequest?
var animatedAsset: ProxiedContentAsset?
var imageView: YYAnimatedImageView?
var imageView: AnimatedImageView?
var activityIndicator: UIActivityIndicatorView?
var isCellSelected: Bool = false {
@ -206,13 +206,8 @@ class GifPickerCell: UICollectionViewCell {
clearViewState()
return
}
guard let image = YYImage(contentsOfFile: asset.filePath) else {
Log.error(.giphy, "Cell could not load asset.")
clearViewState()
return
}
if imageView == nil {
let imageView = YYAnimatedImageView()
let imageView = AnimatedImageView()
self.imageView = imageView
self.contentView.addSubview(imageView)
imageView.pin(to: contentView)
@ -222,7 +217,7 @@ class GifPickerCell: UICollectionViewCell {
clearViewState()
return
}
imageView.image = image
imageView.loadAnimatedImage(from: URL(fileURLWithPath: asset.filePath))
imageView.accessibilityIdentifier = "gif cell"
self.themeBackgroundColor = nil

@ -3,7 +3,6 @@
import UIKit
import AVKit
import AVFoundation
import YYImage
import SessionUIKit
import SignalUtilitiesKit
import SessionMessagingKit
@ -132,8 +131,8 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate {
}
public func parentDidAppear() {
if mediaView is YYAnimatedImageView {
(mediaView as? YYAnimatedImageView)?.startAnimating()
if mediaView is AnimatedImageView {
(mediaView as? AnimatedImageView)?.startAnimating()
}
if self.galleryItem.attachment.isVideo {
@ -160,7 +159,6 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate {
let maybeImageSize: CGSize? = {
switch self.mediaView {
case let imageView as UIImageView: return (imageView.image?.size ?? .zero)
case let imageView as YYAnimatedImageView: return (imageView.image?.size ?? .zero)
default: return nil
}
}()
@ -204,9 +202,9 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate {
if self.galleryItem.attachment.isAnimated {
if self.galleryItem.attachment.isValid, let originalFilePath: String = self.galleryItem.attachment.originalFilePath(using: dependencies) {
let animatedView: YYAnimatedImageView = YYAnimatedImageView()
animatedView.autoPlayAnimatedImage = false
animatedView.image = YYImage(contentsOfFile: originalFilePath)
let animatedView: AnimatedImageView = AnimatedImageView()
animatedView.loadAnimatedImage(from: originalFilePath)
animatedView.startAnimating()
self.mediaView = animatedView
}
else {

@ -0,0 +1,91 @@
# Building
We typically develop against the latest stable version of Xcode.
As of this writing, that's Xcode 12.4
## Prerequistes
Install [CocoaPods](https://guides.cocoapods.org/using/getting-started.html).
## 1. Clone
Clone the repo to a working directory:
```
git clone https://github.com/oxen-io/session-ios.git
```
**Recommendation:**
We recommend you fork the repo on GitHub, then clone your fork:
```
git clone https://github.com/<USERNAME>/session-ios.git
```
You can then add the Session repo to sync with upstream changes:
```
git remote add upstream https://github.com/oxen-io/session-ios
```
## 2. Submodules
Session requires a number of submodules to build, these can be retrieved by navigating to the project directory and running:
```
git submodule update --init --recursive
```
## 3. libSession build dependencies
The iOS project has a share C++ library called `libSession` which is built as one of the project dependencies, in order for this to compile the following dependencies need to be installed:
- cmake
- m4
- pkg-config
These can be installed with Homebrew via `brew install cmake m4 pkg-config`
Additionally `xcode-select` needs to be setup correctly (depending on the order of installation it can point to the wrong directory and result in a build error similar to `tool '{name}' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance`), this can be setup correctly by running:
`sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`
## 4. Xcode
Open the `Session.xcodeproj` in Xcode.
```
open Session.xcodeproj
```
In the TARGETS area of the General tab, change the Team dropdown to
your own. You will need to do that for all the listed targets, e.g.
Session, SessionShareExtension, and SessionNotificationServiceExtension. You
will need an Apple Developer account for this.
On the Capabilities tab, turn off Push Notifications and Data Protection,
while keeping Background Modes on. The App Groups capability will need to
remain on in order to access the shared data storage.
Build and Run and you are ready to go!
## Known issues
### Address & Undefined Behaviour Sanitizer Linker Errors
It seems that there is an open issue with Swift Package Manager (https://github.com/swiftlang/swift-package-manager/issues/4407) where some packages (in our case `libwebp`) run into issues when the Address Sanitizer or Undefined Behaviour Sanitizer are enabled within the scheme, if you see linker errors like the below when building this is likely the issue and can be resolved by disabling these sanitisers.
In order to still benefit from these settings they are explicitly set as `Other C Flags` for the `SessionUtil` target when building in debug mode to enable better debugging of `libSession`.
```
Undefined symbol: ___asan_init
Undefined symbol: ___ubsan_handle_add_overflow
```
### Third-party Installation
The database for the app is stored within an `App Group` directory which is based on the app identifier, unfortunately the identifier cannot be retrieved at runtime so it's currently hard-coded in the code. In order to be able to run session on a device you will need to update the `UserDefaults.applicationGroup` variable in `SessionUtilitiesKit/General/SNUserDefaults` to match the value provided (You may also need to create the `App Group` on your Apple Developer account).
### Push Notifications
Features related to push notifications are known to be not working for
third-party contributors since Apple's Push Notification service pushes
will only work with the Session production code signing
certificate.

@ -955,42 +955,6 @@ Public License instead of this License. But first, please read
<key>Title</key>
<string>libsession-util-spm</string>
</dict>
<dict>
<key>License</key>
<string>Copyright (c) 2010, Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Google nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</string>
<key>Title</key>
<string>libwebp-Xcode - libwebp</string>
</dict>
<dict>
<key>License</key>
<string>Apache License
@ -1679,34 +1643,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
<key>Title</key>
<string>session-grdb-swift</string>
</dict>
<dict>
<key>License</key>
<string>The MIT License (MIT)
Copyright (c) 2015 ibireme &lt;ibireme@gmail.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</string>
<key>Title</key>
<string>session-ios-yyimage</string>
</dict>
<dict>
<key>License</key>
<string>ISC License

@ -3,7 +3,6 @@
import Foundation
import Combine
import GRDB
import YYImage
import DifferenceKit
import SessionUIKit
import SessionMessagingKit

@ -875,14 +875,36 @@ extension Attachment {
}
private func loadThumbnail(with dimensions: UInt, using dependencies: Dependencies, success: @escaping (UIImage, () throws -> Data) -> (), failure: @escaping () -> ()) {
guard let width: UInt = self.width, let height: UInt = self.height, width > 1, height > 1 else {
failure()
return
}
guard
let targetSize: CGSize = {
guard
let width: UInt = self.width,
let height: UInt = self.height,
width > 1,
height > 1
else {
guard let filePath: String = self.originalFilePath(using: dependencies) else {
return .zero
}
let fallbackSize: CGSize = Data.imageSize(for: filePath, type: UTType(sessionMimeType: contentType), using: dependencies)
guard fallbackSize.width > 1 && fallbackSize.height > 1 else {
return .zero
}
return fallbackSize
}
return CGSize(width: Int(width), height: Int(height))
}(),
targetSize.width > 1 &&
targetSize.height > 1
else { return failure() }
// There's no point in generating a thumbnail if the original is smaller than the
// thumbnail size
if width < dimensions || height < dimensions {
if Int(targetSize.width) < dimensions || Int(targetSize.height) < dimensions {
guard let image: UIImage = originalImage(using: dependencies) else {
failure()
return

@ -0,0 +1,129 @@
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import ImageIO
public class AnimatedImageView: UIImageView {
private var imageSource: CGImageSource?
private var frameCount: Int = 0
private var frameDurations: [TimeInterval] = []
private var totalDuration: TimeInterval = 0
private var displayLink: CADisplayLink?
private var currentFrame: Int = 0
private var currentTime: TimeInterval = 0
// MARK: - Functions
public func loadAnimatedImage(from path: String) {
loadAnimatedImage(from: URL(fileURLWithPath: path))
}
public func loadAnimatedImage(from url: URL) {
guard let imageSource: CGImageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { return }
loadAnimatedImage(from: imageSource)
}
public func loadAnimatedImage(from data: Data?) {
guard
let data: Data = data,
let imageSource: CGImageSource = CGImageSourceCreateWithData(data as CFData, nil)
else { return }
loadAnimatedImage(from: imageSource)
}
// MARK: - Internal Functions
private func loadAnimatedImage(from source: CGImageSource) {
self.imageSource = source
self.frameCount = CGImageSourceGetCount(source)
guard frameCount > 1 else {
self.image = createImage(at: 0)
return
}
calculateFrameDurations()
startAnimation()
}
private func calculateFrameDurations() {
frameDurations = []
totalDuration = 0
for i in 0..<frameCount {
let duration = frameDuration(at: i)
frameDurations.append(duration)
totalDuration += duration
}
}
private func frameDuration(at index: Int) -> TimeInterval {
guard let imageSource: CGImageSource = imageSource, index < frameCount else { return 0.1 }
guard let frameProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil) as? [String: Any] else {
return 0.1
}
if let gifProps = frameProperties[kCGImagePropertyGIFDictionary as String] as? [String: Any],
let delayTime = gifProps[kCGImagePropertyGIFDelayTime as String] as? Double {
return delayTime > 0 ? delayTime : 0.1
}
if let webpProps = frameProperties[kCGImagePropertyWebPDictionary as String] as? [String: Any],
let delayTime = webpProps[kCGImagePropertyWebPDelayTime as String] as? Double {
return delayTime > 0 ? delayTime : 0.1
}
return 0.1
}
private func createImage(at index: Int) -> UIImage? {
guard
let imageSource: CGImageSource = imageSource,
index < frameCount,
let cgImage: CGImage = CGImageSourceCreateImageAtIndex(imageSource, index, nil)
else { return nil }
return UIImage(cgImage: cgImage)
}
private func startAnimation() {
stopAnimation()
currentFrame = 0
currentTime = 0
// Set the initial frame
if let image: UIImage = createImage(at: 0) {
self.image = image
}
// Add a display link callback to trigger the frame changes
displayLink = CADisplayLink(target: self, selector: #selector(updateFrame))
displayLink?.add(to: .main, forMode: .common)
}
private func stopAnimation() {
displayLink?.invalidate()
displayLink = nil
}
@objc private func updateFrame(displayLink: CADisplayLink) {
currentTime += displayLink.duration
if currentTime >= frameDurations[currentFrame] {
currentTime = 0
currentFrame = (currentFrame + 1) % frameCount
if let image: UIImage = createImage(at: currentFrame) {
self.image = image
}
}
}
public override func removeFromSuperview() {
stopAnimation()
super.removeFromSuperview()
}
}

@ -2,7 +2,6 @@
import UIKit
import Combine
import YYImage
public final class ProfilePictureView: UIView {
public struct Info {
@ -203,8 +202,8 @@ public final class ProfilePictureView: UIView {
return result
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
private lazy var animatedImageView: AnimatedImageView = {
let result: AnimatedImageView = AnimatedImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
@ -234,8 +233,8 @@ public final class ProfilePictureView: UIView {
return result
}()
private lazy var additionalAnimatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
private lazy var additionalAnimatedImageView: AnimatedImageView = {
let result: AnimatedImageView = AnimatedImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
@ -502,9 +501,12 @@ public final class ProfilePictureView: UIView {
// Populate the main imageView
switch (info.imageData, info.imageData?.suiKitGuessedImageFormat) {
case (.some(let data), .gif), (.some(let data), .webp):
animatedImageView.image = YYImage(data: data)
imageView.image = nil
animatedImageView.loadAnimatedImage(from: data)
case (.some(let data), _):
animatedImageView.image = nil
switch info.renderingMode {
case .automatic: imageView.image = UIImage(data: data)
default:
@ -557,7 +559,7 @@ public final class ProfilePictureView: UIView {
// Set the additional image content and reposition the image views correctly
switch (additionalInfo.imageData, additionalInfo.imageData?.suiKitGuessedImageFormat) {
case (.some(let data), .gif), (.some(let data), .webp):
additionalAnimatedImageView.image = YYImage(data: data)
additionalAnimatedImageView.loadAnimatedImage(from: data)
case (.some(let data), _):
switch additionalInfo.renderingMode {

@ -3,7 +3,6 @@
import UIKit
import ImageIO
import UniformTypeIdentifiers
import libwebp
public extension Data {
private struct ImageDimensions {
@ -83,26 +82,37 @@ public extension Data {
}
var sizeForWebpData: CGSize {
withUnsafeBytes { (unsafeBytes: UnsafeRawBufferPointer) -> CGSize in
guard let bytes: UnsafePointer<UInt8> = unsafeBytes.bindMemory(to: UInt8.self).baseAddress else {
return .zero
}
var webPData: WebPData = WebPData()
webPData.bytes = bytes
webPData.size = unsafeBytes.count
guard let demuxer: OpaquePointer = WebPDemux(&webPData) else { return .zero }
let canvasWidth: UInt32 = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH)
let canvasHeight: UInt32 = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT)
let frameCount: UInt32 = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT)
WebPDemuxDelete(demuxer)
guard canvasWidth > 0 && canvasHeight > 0 && frameCount > 0 else { return .zero }
return CGSize(width: Int(canvasWidth), height: Int(canvasHeight))
guard let source: CGImageSource = CGImageSourceCreateWithData(self as CFData, nil) else {
return .zero
}
// Check if there's at least one image
let count: Int = CGImageSourceGetCount(source)
guard count > 0 else {
return .zero
}
// Get properties of the first frame
guard let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
return .zero
}
// Try to get dimensions from properties
if
let width: Int = properties[kCGImagePropertyPixelWidth] as? Int,
let height: Int = properties[kCGImagePropertyPixelHeight] as? Int,
width > 0,
height > 0
{
return CGSize(width: width, height: height)
}
// If we can't get dimensions from properties, try creating an image
if let image: CGImage = CGImageSourceCreateImageAtIndex(source, 0, nil) {
return CGSize(width: image.width, height: image.height)
}
return .zero
}
// MARK: - Initialization

@ -129,7 +129,18 @@ public extension UTType {
case "audio/aac", "audio/x-m4a": self = .mpeg4Audio
case "audio/aiff", "audio/x-aiff": self = .aiff
default: return nil
default:
/// It's possible we were given a UTI instead of a mimeType so try to retrieve the `preferredMIMEType`
/// from the OS by processing the value directly as a `UTType` and direct that back through the
/// `UTType(sessionMimeType:)` to use our desired behaviour
guard
let fallbackType: UTType = UTType(sessionMimeType),
let mimeType: String = fallbackType.preferredMIMEType,
mimeType != sessionMimeType,
let result: UTType = UTType(sessionMimeType: mimeType)
else { return nil }
self = result
}
return

@ -3,7 +3,6 @@
import UIKit
import Combine
import MediaPlayer
import YYImage
import NVActivityIndicatorView
import SessionUIKit
import SessionMessagingKit
@ -51,19 +50,23 @@ public class MediaMessageView: UIView {
return nil
}()
private lazy var validAnimatedImage: YYImage? = {
private lazy var validAnimatedImageData: Data? = {
guard
attachment.isAnimatedImage,
attachment.isValidImage,
let dataUrl: URL = attachment.dataUrl,
let image: YYImage = YYImage(contentsOfFile: dataUrl.path),
image.size.width > 0,
image.size.height > 0
else {
return nil
}
let imageData: Data = try? Data(contentsOf: dataUrl), (
(
attachment.dataType == .gif &&
imageData.hasValidGifSize
) || (
attachment.dataType == .webP &&
imageData.sizeForWebpData != .zero
)
)
else { return nil }
return image
return imageData
}()
private lazy var duration: TimeInterval? = attachment.duration()
private var linkPreviewInfo: (url: String, draft: LinkPreviewDraft?)?
@ -172,13 +175,13 @@ public class MediaMessageView: UIView {
return view
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let view: YYAnimatedImageView = YYAnimatedImageView()
private lazy var animatedImageView: AnimatedImageView = {
let view: AnimatedImageView = AnimatedImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
if let image: YYImage = validAnimatedImage {
view.image = image
if let imageData: Data = validAnimatedImageData {
view.loadAnimatedImage(from: imageData)
}
else {
view.contentMode = .scaleAspectFit
@ -408,7 +411,7 @@ public class MediaMessageView: UIView {
// If we don't have a valid image then use the 'generic' case
}
else if attachment.isAnimatedImage {
if validAnimatedImage != nil { return nil }
if validAnimatedImageData != nil { return nil }
// If we don't have a valid image then use the 'generic' case
}

Loading…
Cancel
Save