Skip to content

Commit

Permalink
Merge pull request #1771 from nextcloud/blurhash
Browse files Browse the repository at this point in the history
Add blurhash as placeholder
  • Loading branch information
Ivansss authored Dec 16, 2024
2 parents f3f79c0 + 7c58520 commit bea8547
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 28 deletions.
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 @@ -2900,9 +2900,9 @@ import SwiftUI
} 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 @@ import SwiftUI
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

0 comments on commit bea8547

Please sign in to comment.