Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NextcloudTalk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
1F205C572CEFA01900AAA673 /* OutOfOfficeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205C562CEFA01900AAA673 /* OutOfOfficeView.swift */; };
1F205D412CFC6DD300AAA673 /* AvatarProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205D402CFC6DCF00AAA673 /* AvatarProtocol.swift */; };
1F205D442CFC70AD00AAA673 /* AvatarProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205D402CFC6DCF00AAA673 /* AvatarProtocol.swift */; };
1F21A0502C747FC500ED8C0C /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F21A04F2C747FC500ED8C0C /* BlurHashDecode.swift */; };
1F24B5A228E0648600654457 /* ReferenceGithubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F24B5A128E0648600654457 /* ReferenceGithubView.swift */; };
1F24B5A428E0649200654457 /* ReferenceGithubView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F24B5A328E0649200654457 /* ReferenceGithubView.xib */; };
1F35F8E22AEEBAF900044BDA /* InputbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5A24322ADA77DA009939FE /* InputbarViewController.swift */; };
Expand Down Expand Up @@ -706,6 +707,7 @@
1F21A06B2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
1F21A06C2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = "<group>"; };
1F21A06D2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
1F21A04F2C747FC500ED8C0C /* BlurHashDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
1F24B5A128E0648600654457 /* ReferenceGithubView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceGithubView.swift; sourceTree = "<group>"; };
1F24B5A328E0649200654457 /* ReferenceGithubView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceGithubView.xib; sourceTree = "<group>"; };
1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerExtension.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1695,6 +1697,7 @@
2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */,
2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */,
1F1B0F352BDD8B9C003FD766 /* NCActivityIndicator.swift */,
1F21A04F2C747FC500ED8C0C /* BlurHashDecode.swift */,
);
name = "User Interface";
sourceTree = "<group>";
Expand Down Expand Up @@ -2875,6 +2878,7 @@
2C0574851EDD9E8E00D9E7F2 /* AppDelegate.m in Sources */,
2C4987BD21E640E20060AC27 /* CallKitManager.m in Sources */,
1F1B0F4C2BE18FF3003FD766 /* CustomPresentableNavigationController.swift in Sources */,
1F21A0502C747FC500ED8C0C /* BlurHashDecode.swift in Sources */,
2C4446F3265D51A600DF1DBC /* NCPushNotificationsUtils.m in Sources */,
2C0424902CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift in Sources */,
2C1ABDE5257F883400AEDFB6 /* ABContact.m in Sources */,
Expand Down
46 changes: 31 additions & 15 deletions NextcloudTalk/BaseChatTableViewCell+File.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,30 @@ extension BaseChatTableViewCell {
return
}

var placeholderImage: UIImage?
var previewImageHeight: CGFloat?
var previewImageWidth: CGFloat?

// In case we can determine the height before requesting the preview, adjust the imageView constraints accordingly
if file.previewImageHeight > 0 {
self.filePreviewImageViewHeightConstraint?.constant = CGFloat(file.previewImageHeight)
if file.previewImageHeight > 0 && file.previewImageWidth > 0 {
previewImageHeight = CGFloat(file.previewImageHeight)
previewImageWidth = CGFloat(file.previewImageWidth)
} else {
let estimatedPreviewHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message)
let estimatedPreviewSize = BaseChatTableViewCell.getEstimatedPreviewSize(for: message)

if estimatedPreviewSize.height > 0 && estimatedPreviewSize.width > 0 {
previewImageHeight = estimatedPreviewSize.height
previewImageWidth = estimatedPreviewSize.width
}
}

if let previewImageHeight, let previewImageWidth {
self.filePreviewImageViewHeightConstraint?.constant = previewImageHeight
self.filePreviewImageViewWidthConstraint?.constant = previewImageWidth

if estimatedPreviewHeight > 0 {
self.filePreviewImageViewHeightConstraint?.constant = estimatedPreviewHeight
if !message.isAnimatableGif, let blurhash = message.file()?.blurhash {
let aspectRatio = previewImageHeight / previewImageWidth
placeholderImage = .init(blurHash: blurhash, size: .init(width: 20, height: 20 * aspectRatio))
}
}

Expand All @@ -140,7 +156,7 @@ extension BaseChatTableViewCell {
if message.isAnimatableGif {
self.requestGifPreview(for: message, with: account)
} else {
self.requestDefaultPreview(for: message, with: account)
self.requestDefaultPreview(for: message, withPlaceholderImage: placeholderImage, with: account)
}
}

Expand All @@ -162,7 +178,7 @@ extension BaseChatTableViewCell {
let gifImage = try? UIImage(gifData: data), let baseImage = UIImage(data: data) else {

// No gif, try to request a normal preview
self.requestDefaultPreview(for: message, with: account)
self.requestDefaultPreview(for: message, withPlaceholderImage: nil, with: account)
return
}

Expand All @@ -171,13 +187,13 @@ extension BaseChatTableViewCell {
}
}

func requestDefaultPreview(for message: NCChatMessage, with account: TalkAccount) {
func requestDefaultPreview(for message: NCChatMessage, withPlaceholderImage placeholderImage: UIImage?, with account: TalkAccount) {
guard let file = message.file() else { return }

let requestedHeight = Int(3 * fileMessageCellFileMaxPreviewHeight)
guard let previewRequest = NCAPIController.sharedInstance().createPreviewRequest(forFile: file.parameterId, withMaxHeight: requestedHeight, using: account) else { return }

self.filePreviewImageView?.setImageWith(previewRequest, placeholderImage: nil, success: { [weak self] _, _, image in
self.filePreviewImageView?.setImageWith(previewRequest, placeholderImage: placeholderImage, success: { [weak self] _, _, image in
guard let self, let imageView = self.filePreviewImageView else { return }

imageView.image = image
Expand Down Expand Up @@ -218,7 +234,7 @@ extension BaseChatTableViewCell {
self.filePreviewPlayIconImageView?.center = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0)
}

self.delegate?.cellHasDownloadedImagePreview(withHeight: ceil(previewSize.height), for: message)
self.delegate?.cellHasDownloadedImagePreview(withSize: .init(width: ceil(previewSize.width), height: ceil(previewSize.height)), for: message)
}

func showFallbackIcon(for message: NCChatMessage) {
Expand Down Expand Up @@ -284,22 +300,22 @@ extension BaseChatTableViewCell {
return CGSize(width: width, height: height)
}

static func getEstimatedPreviewSize(for message: NCChatMessage?) -> CGFloat {
guard let message, let fileParameter = message.file() else { return 0 }
static func getEstimatedPreviewSize(for message: NCChatMessage?) -> CGSize {
guard let message, let fileParameter = message.file() else { return .zero }

// We don't have any information about the image to display
if fileParameter.width == 0 && fileParameter.height == 0 {
return 0
return .zero
}

// We can only estimate the height for images and videos
if !NCUtils.isVideo(fileType: fileParameter.mimetype), !NCUtils.isImage(fileType: fileParameter.mimetype) {
return 0
return .zero
}

let imageSize = CGSize(width: CGFloat(fileParameter.width), height: CGFloat(fileParameter.height))
let previewSize = self.getPreviewSize(from: imageSize, true)

return ceil(previewSize.height)
return .init(width: ceil(previewSize.width), height: ceil(previewSize.height))
}
}
2 changes: 1 addition & 1 deletion NextcloudTalk/BaseChatTableViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ protocol BaseChatTableViewCellDelegate: AnyObject {
func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage)

func cellWants(toDownloadFile fileParameter: NCMessageFileParameter, for message: NCChatMessage)
func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage)
func cellHasDownloadedImagePreview(withSize size: CGSize, for message: NCChatMessage)

func cellWants(toOpenLocation geoLocationRichObject: GeoLocationRichObject)

Expand Down
12 changes: 6 additions & 6 deletions NextcloudTalk/BaseChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1903,7 +1903,7 @@
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}

func handleLongPressInVoiceMessageRecordButton(gestureRecognizer: UILongPressGestureRecognizer) {

Check warning on line 1906 in NextcloudTalk/BaseChatViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Cyclomatic Complexity Violation: Function should have complexity 10 or less; currently complexity is 13 (cyclomatic_complexity)
if self.rightButton.tag != sendButtonTagVoice {
return
}
Expand Down Expand Up @@ -2900,9 +2900,9 @@
} else if let file = message.file() {
if file.previewImageHeight > 0 {
height += CGFloat(file.previewImageHeight)
} else if case let estimatedHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message), estimatedHeight > 0 {
height += estimatedHeight
message.setPreviewImageHeight(estimatedHeight)
} else if case let estimatedSize = BaseChatTableViewCell.getEstimatedPreviewSize(for: message), estimatedSize.height > 0 {
height += estimatedSize.height
message.setPreviewImageSize(estimatedSize)
} else {
height += fileMessageCellFileMaxPreviewHeight
}
Expand Down Expand Up @@ -3374,14 +3374,14 @@
downloader.downloadFile(fromMessage: fileParameter)
}

public func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage) {
if message.file().previewImageHeight == Int(height) {
public func cellHasDownloadedImagePreview(withSize size: CGSize, for message: NCChatMessage) {
if message.file().previewImageHeight == Int(size.height) {
return
}

let isAtBottom = self.shouldScrollOnNewMessages()

message.setPreviewImageHeight(height)
message.setPreviewImageSize(size)

CATransaction.begin()
CATransaction.setCompletionBlock {
Expand Down
151 changes: 151 additions & 0 deletions NextcloudTalk/BlurHashDecode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// SPDX-FileCopyrightText: 2018 Wolt Enterprises
// SPDX-License-Identifier: MIT
//

import UIKit

extension UIImage {
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil }

let sizeFlag = String(blurHash[0]).decode83()
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1

let quantisedMaximumValue = String(blurHash[1]).decode83()
let maximumValue = Float(quantisedMaximumValue + 1) / 166

guard blurHash.count == 4 + 2 * numX * numY else { return nil }

let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value)
} else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch)
}
}

let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }

for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0

for j in 0 ..< numY {
for i in 0 ..< numX {
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
let colour = colours[i + j * numX]
r += colour.0 * basis
g += colour.1 * basis
b += colour.2 * basis
}
}

let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))

pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB
}
}

let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)

guard let provider = CGDataProvider(data: data) else { return nil }
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }

self.init(cgImage: cgImage)
}
}

private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
}

private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19

let rgb = (
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
)

return rgb
}

private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value)
}

private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
}

private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 }
else { return pow((v + 0.055) / 1.055, 2.4) }
}

private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}()

private let decodeCharacters: [String: Int] = {
var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
}()

extension String {
func decode83() -> Int {
var value: Int = 0
for character in self {
if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit
}
}
return value
}
}

private extension String {
subscript (offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)]
}

subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}

subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
}
2 changes: 1 addition & 1 deletion NextcloudTalk/NCChatMessage.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ typedef void (^GetReferenceDataCompletionBlock)(NCChatMessage *message, NSDictio
- (NSArray<NCChatReaction *> * _Nonnull)reactionsArray;
- (BOOL)containsURL;
- (void)getReferenceDataWithCompletionBlock:(GetReferenceDataCompletionBlock _Nullable)block;
- (void)setPreviewImageHeight:(CGFloat)height;
- (void)setPreviewImageSize:(CGSize)size;

// Public for swift extension
- (NSMutableArray * _Nonnull)temporaryReactions;
Expand Down
17 changes: 12 additions & 5 deletions NextcloudTalk/NCChatMessage.m
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ + (instancetype)messageWithDictionary:(NSDictionary *)messageDict andAccountId:(
+ (void)updateChatMessage:(NCChatMessage *)managedChatMessage withChatMessage:(NCChatMessage *)chatMessage isRoomLastMessage:(BOOL)isRoomLastMessage
{
int previewImageHeight = 0;
int previewImageWidth = 0;

// Try to keep our locally saved previewImageHeight when updating this messages with the server message
// This happens when updating the last message of a room for example
Expand All @@ -149,6 +150,10 @@ + (void)updateChatMessage:(NCChatMessage *)managedChatMessage withChatMessage:(N
if (managedChatMessage.file.previewImageHeight > 0 && chatMessage.file.previewImageHeight == 0) {
previewImageHeight = managedChatMessage.file.previewImageHeight;
}

if (managedChatMessage.file.previewImageWidth > 0 && chatMessage.file.previewImageWidth == 0) {
previewImageWidth = managedChatMessage.file.previewImageWidth;
}
}

managedChatMessage.actorDisplayName = chatMessage.actorDisplayName;
Expand Down Expand Up @@ -176,8 +181,8 @@ + (void)updateChatMessage:(NCChatMessage *)managedChatMessage withChatMessage:(N
managedChatMessage.parentId = chatMessage.parentId;
}

if (previewImageHeight > 0) {
[managedChatMessage setPreviewImageHeight:previewImageHeight];
if (previewImageHeight > 0 && previewImageWidth > 0) {
[managedChatMessage setPreviewImageSize:CGSizeMake(previewImageWidth, previewImageHeight)];
}
}

Expand Down Expand Up @@ -623,7 +628,7 @@ - (void)getReferenceDataWithCompletionBlock:(GetReferenceDataCompletionBlock)blo
}
}

- (void)setPreviewImageHeight:(CGFloat)height
- (void)setPreviewImageSize:(CGSize)size
{
// Since the messageParameters property is a non-mutable dictionary, we create a mutable copy
NSMutableDictionary *messageParameterDict = [[NSMutableDictionary alloc] initWithDictionary:self.messageParameters];
Expand All @@ -634,7 +639,8 @@ - (void)setPreviewImageHeight:(CGFloat)height
}

[messageParameterDict setObject:fileParameterDict forKey:@"file"];
[fileParameterDict setObject:@(height) forKey:@"preview-image-height"];
[fileParameterDict setObject:@(size.height) forKey:@"preview-image-height"];
[fileParameterDict setObject:@(size.width) forKey:@"preview-image-width"];

NSData *jsonData = [NSJSONSerialization dataWithJSONObject:messageParameterDict
options:0
Expand All @@ -648,7 +654,8 @@ - (void)setPreviewImageHeight:(CGFloat)height

// Since we previously accessed the 'file' property, it would not be created from the JSON String again
// Manually set it for the lifetime of this message
self.file.previewImageHeight = height;
self.file.previewImageHeight = size.height;
self.file.previewImageWidth = size.width;

// Save our changes to the database
RLMRealm *realm = [RLMRealm defaultRealm];
Expand Down
Loading
Loading