From 37af7bdcf2618cd9c72937ca1fa14b38d164146f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sun, 17 Mar 2024 22:58:04 +0100 Subject: [PATCH 01/11] Dispatch non-important tasks to the background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/NCSettingsController.m | 8 +++++++- NextcloudTalk/NCUtils.swift | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/NextcloudTalk/NCSettingsController.m b/NextcloudTalk/NCSettingsController.m index 205ed6a8c..a80c33f54 100644 --- a/NextcloudTalk/NCSettingsController.m +++ b/NextcloudTalk/NCSettingsController.m @@ -107,12 +107,18 @@ - (id)init _externalSignalingControllers = [NSMutableDictionary new]; [self configureDatabase]; - [self createAccountsFile]; [self checkStoredDataInKechain]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tokenRevokedResponseReceived:) name:NCTokenRevokedResponseReceivedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(upgradeRequiredResponseReceived:) name:NCUpgradeRequiredResponseReceivedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(talkConfigurationHasChanged:) name:NCTalkConfigurationHashChangedNotification object:nil]; + + // No need to create the file immediately -> dispatch to background + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^(void){ + @autoreleasepool { + [self createAccountsFile]; + } + }); } return self; } diff --git a/NextcloudTalk/NCUtils.swift b/NextcloudTalk/NCUtils.swift index 8e7e35c97..4a3d25676 100644 --- a/NextcloudTalk/NCUtils.swift +++ b/NextcloudTalk/NCUtils.swift @@ -469,7 +469,9 @@ import UniformTypeIdentifiers public static func log(_ message: String) { do { - self.removeOldLogfiles() + DispatchQueue.global(qos: .background).async { + self.removeOldLogfiles() + } guard let logPath = self.getLogfilePath() else { return } From 8a50b4bb6d8b5694262e2246f631e5ce120c32f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sun, 17 Mar 2024 22:58:28 +0100 Subject: [PATCH 02/11] Remove iOS 14.5 check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/AppDelegate.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/NextcloudTalk/AppDelegate.m b/NextcloudTalk/AppDelegate.m index ce618dbbd..b1b830c02 100644 --- a/NextcloudTalk/AppDelegate.m +++ b/NextcloudTalk/AppDelegate.m @@ -89,10 +89,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [NCUserInterfaceController sharedInstance].mainViewController = (NCSplitViewController *) self.window.rootViewController; [NCUserInterfaceController sharedInstance].roomsTableViewController = [NCUserInterfaceController sharedInstance].mainViewController.viewControllers.firstObject.childViewControllers.firstObject; - - if (@available(iOS 14.5, *)) { - [NCUserInterfaceController sharedInstance].mainViewController.displayModeButtonVisibility = UISplitViewControllerDisplayModeButtonVisibilityNever; - } + [NCUserInterfaceController sharedInstance].mainViewController.displayModeButtonVisibility = UISplitViewControllerDisplayModeButtonVisibilityNever; NSArray *arguments = [[NSProcessInfo processInfo] arguments]; From 52d432edd14073dba67f709b96a1fac5d2faf569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sun, 17 Mar 2024 22:58:54 +0100 Subject: [PATCH 03/11] Make sure AvatarButton is always round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/AvatarButton.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NextcloudTalk/AvatarButton.swift b/NextcloudTalk/AvatarButton.swift index 70e7fa8a6..174f830a9 100644 --- a/NextcloudTalk/AvatarButton.swift +++ b/NextcloudTalk/AvatarButton.swift @@ -43,10 +43,17 @@ import SDWebImage } private func commonInit() { + self.layer.masksToBounds = true self.imageView?.contentMode = .scaleToFill self.imageView?.frame = self.frame self.contentVerticalAlignment = .fill self.contentHorizontalAlignment = .fill + self.backgroundColor = .systemGray3 + } + + override func layoutSubviews() { + super.layoutSubviews() + self.layer.cornerRadius = self.frame.width / 2.0 } // MARK: - Conversation avatars From 42bf6bd36e3cc6579a525b679b85005d57579bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sun, 17 Mar 2024 23:04:42 +0100 Subject: [PATCH 04/11] Introduce new BaseChatTableViewCell for messages and files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk.xcodeproj/project.pbxproj | 52 +- .../BaseChatTableViewCell+File.swift | 336 ++++++++++ .../BaseChatTableViewCell+Message.swift | 49 ++ NextcloudTalk/BaseChatTableViewCell.swift | 493 ++++++++++++++ NextcloudTalk/BaseChatTableViewCell.xib | 148 +++++ NextcloudTalk/BaseChatViewController.swift | 87 ++- NextcloudTalk/ChatMessageTableViewCell.h | 61 -- NextcloudTalk/ChatMessageTableViewCell.m | 463 -------------- NextcloudTalk/ChatViewController.swift | 9 +- NextcloudTalk/FileMessageTableViewCell.h | 80 --- NextcloudTalk/FileMessageTableViewCell.m | 604 ------------------ NextcloudTalk/FilePreviewImageView.swift | 26 + .../GroupedChatMessageTableViewCell.h | 49 -- .../GroupedChatMessageTableViewCell.m | 239 ------- NextcloudTalk/NCMessageFileParameter.h | 2 +- NextcloudTalk/NCMessageParameter.h | 4 +- ...NextcloudTalk-Bridging-Header-Extensions.h | 1 - NextcloudTalk/NextcloudTalk-Bridging-Header.h | 3 - NextcloudTalk/UIFontExtension.swift | 61 ++ .../Unit/UnitChatCellTest.swift | 9 +- 20 files changed, 1196 insertions(+), 1580 deletions(-) create mode 100644 NextcloudTalk/BaseChatTableViewCell+File.swift create mode 100644 NextcloudTalk/BaseChatTableViewCell+Message.swift create mode 100644 NextcloudTalk/BaseChatTableViewCell.swift create mode 100644 NextcloudTalk/BaseChatTableViewCell.xib delete mode 100644 NextcloudTalk/ChatMessageTableViewCell.h delete mode 100644 NextcloudTalk/ChatMessageTableViewCell.m delete mode 100644 NextcloudTalk/FileMessageTableViewCell.h delete mode 100644 NextcloudTalk/FileMessageTableViewCell.m create mode 100644 NextcloudTalk/FilePreviewImageView.swift delete mode 100644 NextcloudTalk/GroupedChatMessageTableViewCell.h delete mode 100644 NextcloudTalk/GroupedChatMessageTableViewCell.m create mode 100644 NextcloudTalk/UIFontExtension.swift diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index 1226d951a..83b1333d4 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -17,6 +17,10 @@ 1F0ECBF72A68277000921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF62A68277000921E90 /* CDMarkdownKit */; }; 1F0ECBF92A68277C00921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF82A68277C00921E90 /* CDMarkdownKit */; }; 1F11FB7229C07B04001E21E7 /* NCZoomableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */; }; + 1F1B50342B8E069800B0F2F4 /* BaseChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50332B8E069800B0F2F4 /* BaseChatTableViewCell.swift */; }; + 1F1B50382B8E070100B0F2F4 /* BaseChatTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F1B50372B8E070100B0F2F4 /* BaseChatTableViewCell.xib */; }; + 1F1B503A2B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50392B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift */; }; + 1F1B503E2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B503D2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift */; }; 1F1B50442B9095D100B0F2F4 /* FederatedCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */; }; 1F1B50472B90CDF800B0F2F4 /* TalkCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */; }; 1F1B50482B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */; }; @@ -84,6 +88,7 @@ 1F4DD3EC2571C688007DC98E /* EmojiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */; }; 1F4DD3ED2571C688007DC98E /* EmojiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */; }; 1F53819129195FA4003DA6B7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CA1CCAB1F067F35002FE6A2 /* Images.xcassets */; }; + 1F5683CF2BA7980C0023E151 /* FilePreviewImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5683CE2BA7980C0023E151 /* FilePreviewImageView.swift */; }; 1F5813F828EB23EF00318FC3 /* NCSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5813F628EB23EF00318FC3 /* NCSplitViewController.swift */; }; 1F5813F928EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5813F728EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift */; }; 1F59446225B8EDF5002AD65F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C7F47AC20289B9600081CC7 /* Localizable.strings */; }; @@ -221,6 +226,10 @@ 1FDDB0DB2AF440E100FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */; }; 1FDE7C9A28DE14A200CB718E /* ReferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDE7C9928DE14A200CB718E /* ReferenceView.swift */; }; 1FDE7C9C28DE14B000CB718E /* ReferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FDE7C9B28DE14B000CB718E /* ReferenceView.xib */; }; + 1FDFC94D2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; + 1FDFC94E2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; + 1FDFC94F2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; + 1FDFC9502BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; 1FE0C56C2A0531200083576A /* ReferenceTalkView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FE0C56B2A0531200083576A /* ReferenceTalkView.xib */; }; 1FE0C56E2A0531270083576A /* ReferenceTalkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE0C56D2A0531270083576A /* ReferenceTalkView.swift */; }; 1FE94734293CE55600D6584C /* NCCameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE94733293CE55600D6584C /* NCCameraController.swift */; }; @@ -278,7 +287,6 @@ 2C3780C3210F49DC003F9AE8 /* HeaderWithButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3780C2210F49DC003F9AE8 /* HeaderWithButton.m */; }; 2C3780C5210F4A26003F9AE8 /* HeaderWithButton.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C3780C4210F4A26003F9AE8 /* HeaderWithButton.xib */; }; 2C40281522832EED0000DDFC /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; }; - 2C415F9B2136BDD6005F7F37 /* FileMessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C415F9A2136BDD6005F7F37 /* FileMessageTableViewCell.m */; }; 2C4230F72B207AB00013E1FA /* ContextChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */; }; 2C42ADB420B58E6300296DEA /* NCChatController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C42ADB320B58E6300296DEA /* NCChatController.m */; }; 2C43BA7621309A1000B3068A /* NCMessageParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C43BA7521309A1000B3068A /* NCMessageParameter.m */; }; @@ -397,9 +405,7 @@ 2C9B0B9C217F756B00A4752C /* NCNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9B0B9B217F756B00A4752C /* NCNotification.m */; }; 2C9E6CCE1F6F34F000399B7A /* ARDSDPUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E6CCD1F6F34F000399B7A /* ARDSDPUtils.m */; }; 2CA15541208E350300CE8EF0 /* NCChatMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA15540208E350300CE8EF0 /* NCChatMessage.m */; }; - 2CA15548208EA1EA00CE8EF0 /* ChatMessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA15547208EA1EA00CE8EF0 /* ChatMessageTableViewCell.m */; }; 2CA1554B208F2E5700CE8EF0 /* NCMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1554A208F2E5700CE8EF0 /* NCMessageTextView.m */; }; - 2CA155562099E07700CE8EF0 /* GroupedChatMessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA155552099E07700CE8EF0 /* GroupedChatMessageTableViewCell.m */; }; 2CA1CC911F014354002FE6A2 /* NCConnectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CC901F014354002FE6A2 /* NCConnectionController.m */; }; 2CA1CC951F014EF9002FE6A2 /* NCSettingsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CC941F014EF9002FE6A2 /* NCSettingsController.m */; }; 2CA1CC971F016117002FE6A2 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CA1CC961F016117002FE6A2 /* Security.framework */; }; @@ -486,6 +492,7 @@ 2CF8AD402A0010FB00A4D3E6 /* MessageTranslationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF8AD3E2A0010FB00A4D3E6 /* MessageTranslationViewController.xib */; }; 2CF9CBFF26025F65002246EF /* TextInputTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF9CBFB26025F64002246EF /* TextInputTableViewCell.xib */; }; 3FCA62550CD1442D28E8A7C6 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B81BB7A4920C391CC2CACFD /* libPods-NotificationServiceExtension.a */; }; + 4890175925A0D7FC2EC76CC0 /* libPods-NextcloudTalkTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7005E22D6C2896927FC3AEEC /* libPods-NextcloudTalkTests.a */; }; 807E30762A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807E30752A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift */; }; 80832B762A822E5100195A97 /* UserStatusSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80832B752A822E5100195A97 /* UserStatusSwiftUIView.swift */; }; 80832B782A823D0700195A97 /* UserStatusMessageSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80832B772A823D0700195A97 /* UserStatusMessageSwiftUIView.swift */; }; @@ -564,6 +571,10 @@ 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionSuggestion.swift; sourceTree = ""; }; 1F0B0A762BA26BE10073FF8D /* UnitMentionSuggestionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitMentionSuggestionTest.swift; sourceTree = ""; }; 1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCZoomableView.swift; sourceTree = ""; }; + 1F1B50332B8E069800B0F2F4 /* BaseChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseChatTableViewCell.swift; sourceTree = ""; }; + 1F1B50372B8E070100B0F2F4 /* BaseChatTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BaseChatTableViewCell.xib; sourceTree = ""; }; + 1F1B50392B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BaseChatTableViewCell+File.swift"; sourceTree = ""; }; + 1F1B503D2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BaseChatTableViewCell+Message.swift"; sourceTree = ""; }; 1F1B50422B9095C900B0F2F4 /* FederatedCapabilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FederatedCapabilities.h; sourceTree = ""; }; 1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FederatedCapabilities.m; sourceTree = ""; }; 1F1B50452B90CDE600B0F2F4 /* TalkCapabilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TalkCapabilities.h; sourceTree = ""; }; @@ -589,6 +600,7 @@ 1F46CE2828E05B3200E7D88E /* ReferenceDefaultView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceDefaultView.swift; sourceTree = ""; }; 1F46CE2A28E05B3C00E7D88E /* ReferenceDefaultView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceDefaultView.xib; sourceTree = ""; }; 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = ""; }; + 1F5683CE2BA7980C0023E151 /* FilePreviewImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewImageView.swift; sourceTree = ""; }; 1F5813F628EB23EF00318FC3 /* NCSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSplitViewController.swift; sourceTree = ""; }; 1F5813F728EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSplitViewPlaceholderViewController.swift; sourceTree = ""; }; 1F5A24322ADA77DA009939FE /* InputbarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputbarViewController.swift; sourceTree = ""; }; @@ -664,6 +676,7 @@ 1FDDB0E82AFE8F5C00FBAFB7 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 1FDE7C9928DE14A200CB718E /* ReferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferenceView.swift; sourceTree = ""; }; 1FDE7C9B28DE14B000CB718E /* ReferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReferenceView.xib; sourceTree = ""; }; + 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFontExtension.swift; sourceTree = ""; }; 1FE0C56B2A0531200083576A /* ReferenceTalkView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceTalkView.xib; sourceTree = ""; }; 1FE0C56D2A0531270083576A /* ReferenceTalkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceTalkView.swift; sourceTree = ""; }; 1FE94733293CE55600D6584C /* NCCameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCameraController.swift; sourceTree = ""; }; @@ -737,8 +750,6 @@ 2C3780C4210F4A26003F9AE8 /* HeaderWithButton.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HeaderWithButton.xib; sourceTree = ""; }; 2C40281322832EED0000DDFC /* NCDatabaseManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCDatabaseManager.h; sourceTree = ""; }; 2C40281422832EED0000DDFC /* NCDatabaseManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCDatabaseManager.m; sourceTree = ""; }; - 2C415F992136BDD6005F7F37 /* FileMessageTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileMessageTableViewCell.h; sourceTree = ""; }; - 2C415F9A2136BDD6005F7F37 /* FileMessageTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileMessageTableViewCell.m; sourceTree = ""; }; 2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextChatViewController.swift; sourceTree = ""; }; 2C42ADB220B58E6300296DEA /* NCChatController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatController.h; sourceTree = ""; }; 2C42ADB320B58E6300296DEA /* NCChatController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatController.m; sourceTree = ""; }; @@ -896,12 +907,8 @@ 2C9E6CCD1F6F34F000399B7A /* ARDSDPUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDSDPUtils.m; sourceTree = ""; }; 2CA1553F208E350300CE8EF0 /* NCChatMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatMessage.h; sourceTree = ""; }; 2CA15540208E350300CE8EF0 /* NCChatMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatMessage.m; sourceTree = ""; }; - 2CA15546208EA1EA00CE8EF0 /* ChatMessageTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChatMessageTableViewCell.h; sourceTree = ""; }; - 2CA15547208EA1EA00CE8EF0 /* ChatMessageTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChatMessageTableViewCell.m; sourceTree = ""; }; 2CA15549208F2E5700CE8EF0 /* NCMessageTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCMessageTextView.h; sourceTree = ""; }; 2CA1554A208F2E5700CE8EF0 /* NCMessageTextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCMessageTextView.m; sourceTree = ""; }; - 2CA155542099E07700CE8EF0 /* GroupedChatMessageTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GroupedChatMessageTableViewCell.h; sourceTree = ""; }; - 2CA155552099E07700CE8EF0 /* GroupedChatMessageTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GroupedChatMessageTableViewCell.m; sourceTree = ""; }; 2CA1CC8F1F014354002FE6A2 /* NCConnectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCConnectionController.h; sourceTree = ""; }; 2CA1CC901F014354002FE6A2 /* NCConnectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCConnectionController.m; sourceTree = ""; }; 2CA1CC931F014EF9002FE6A2 /* NCSettingsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCSettingsController.h; sourceTree = ""; }; @@ -1030,6 +1037,7 @@ 4F7C31E9D74F550EAF89931B /* libPods-NextcloudTalk.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NextcloudTalk.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 584BF273DF09DE4D5EE0DA0F /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; 684807120F4439797973DF73 /* libPods-ShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7005E22D6C2896927FC3AEEC /* libPods-NextcloudTalkTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NextcloudTalkTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 807E30752A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusOptionsSwiftUI.swift; sourceTree = ""; }; 80832B752A822E5100195A97 /* UserStatusSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusSwiftUIView.swift; sourceTree = ""; }; 80832B772A823D0700195A97 /* UserStatusMessageSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusMessageSwiftUIView.swift; sourceTree = ""; }; @@ -1069,6 +1077,7 @@ 1F759C182B63B9D9000534AB /* SwiftyAttributes in Frameworks */, 1F759C1C2B63B9D9000534AB /* TOCropViewController in Frameworks */, 1F759C1E2B63B9D9000534AB /* SwiftUIIntrospect in Frameworks */, + 4890175925A0D7FC2EC76CC0 /* libPods-NextcloudTalkTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1426,6 +1435,7 @@ 2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */, 2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */, 1FAB2EED2AD1BC1B001214EB /* UIControlExtensions.swift */, + 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */, ); name = "User Interface"; sourceTree = ""; @@ -1662,18 +1672,16 @@ isa = PBXGroup; children = ( 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */, + 1F1B50332B8E069800B0F2F4 /* BaseChatTableViewCell.swift */, + 1F1B50372B8E070100B0F2F4 /* BaseChatTableViewCell.xib */, + 1F1B503D2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift */, + 1F1B50392B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift */, 2CC7159220C54D080045C789 /* ChatTableViewCell.h */, 2CC7159320C54D080045C789 /* ChatTableViewCell.m */, 1F35F9022AEEDEE800044BDA /* AutoCompletionTableViewCell.h */, 1F35F9032AEEDF0E00044BDA /* AutoCompletionTableViewCell.m */, - 2CA15546208EA1EA00CE8EF0 /* ChatMessageTableViewCell.h */, - 2CA15547208EA1EA00CE8EF0 /* ChatMessageTableViewCell.m */, - 2C415F992136BDD6005F7F37 /* FileMessageTableViewCell.h */, - 2C415F9A2136BDD6005F7F37 /* FileMessageTableViewCell.m */, 2CB6ACD02640814100D3D641 /* LocationMessageTableViewCell.h */, 2CB6ACD12640814100D3D641 /* LocationMessageTableViewCell.m */, - 2CA155542099E07700CE8EF0 /* GroupedChatMessageTableViewCell.h */, - 2CA155552099E07700CE8EF0 /* GroupedChatMessageTableViewCell.m */, 2C604BD7211988A700D34DCD /* SystemMessageTableViewCell.h */, 2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */, 2C8E2A19232174C20022BFC9 /* MessageSeparatorTableViewCell.h */, @@ -1683,6 +1691,7 @@ 2C5BFBEB28895E6A00E75118 /* ObjectShareMessageTableViewCell.h */, 2C5BFBEC28895E6B00E75118 /* ObjectShareMessageTableViewCell.m */, 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */, + 1F5683CE2BA7980C0023E151 /* FilePreviewImageView.swift */, ); name = "Chat cells"; sourceTree = ""; @@ -1763,6 +1772,7 @@ 4F7C31E9D74F550EAF89931B /* libPods-NextcloudTalk.a */, 1FF2FD5C2AB99CCB000C9905 /* ReplayKit.framework */, 9A3D305FCD7BF7E727A62F35 /* libPods-BroadcastUploadExtension.a */, + 7005E22D6C2896927FC3AEEC /* libPods-NextcloudTalkTests.a */, ); name = Frameworks; sourceTree = ""; @@ -2270,6 +2280,7 @@ 1FADECDA2B8227B1007AD94B /* FederationInvitationCell.xib in Resources */, 1FEC459C2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib in Resources */, 1FE0C56C2A0531200083576A /* ReferenceTalkView.xib in Resources */, + 1F1B50382B8E070100B0F2F4 /* BaseChatTableViewCell.xib in Resources */, 2C5BFBF828902E3700E75118 /* PollFooterView.xib in Resources */, 1FDE7C9C28DE14B000CB718E /* ReferenceView.xib in Resources */, 2C444707265E59B500DF1DBC /* ShareConfirmationCollectionViewCell.xib in Resources */, @@ -2534,6 +2545,7 @@ 1F77A6242ABA0003007B6037 /* SampleHandler.swift in Sources */, 1F77A5F32AB9A43B007B6037 /* SwiftMarkdownObjCBridge.swift in Sources */, 1F77A6092AB9A593007B6037 /* NCWebImageDownloaderOperation.m in Sources */, + 1FDFC9502BA50B9100670DF4 /* UIFontExtension.swift in Sources */, 1F77A6072AB9A587007B6037 /* NCPushProxySessionManager.m in Sources */, 1FF2FD832AB99F3B000C9905 /* NCAppBranding.m in Sources */, 1F77A5F82AB9A4CD007B6037 /* NCDeckCardParameter.m in Sources */, @@ -2592,6 +2604,7 @@ 2C5BFBEF288A947900E75118 /* PollVotingView.swift in Sources */, 1FAB2EF02AD1EAA3001214EB /* RLMSupport.swift in Sources */, 2CA52AD0267613CB00619610 /* VoiceMessageTableViewCell.m in Sources */, + 1F1B50342B8E069800B0F2F4 /* BaseChatTableViewCell.swift in Sources */, 2C1EF36B25505DCE007C9768 /* NCNavigationController.m in Sources */, DA755811278EF3EF00A48A1B /* UserSettingsTableViewCell.swift in Sources */, 1FA38C9029A4B3C6008871B8 /* NCNotificationAction.swift in Sources */, @@ -2601,7 +2614,6 @@ 2CA1CCC31F166CC5002FE6A2 /* NCRoom.m in Sources */, 2C06BF5D20A89F510031EB46 /* NCRoomsManager.m in Sources */, 2CB6ACCA26401D5200D3D641 /* GeoLocationRichObject.m in Sources */, - 2C415F9B2136BDD6005F7F37 /* FileMessageTableViewCell.m in Sources */, 2C8A2BCE221FEEFE00DE6D2C /* DirectoryTableViewCell.m in Sources */, 2C78EF9C1F826B22008AFA74 /* NCCallController.m in Sources */, 1F1B50442B9095D100B0F2F4 /* FederatedCapabilities.m in Sources */, @@ -2609,6 +2621,7 @@ 2C4D7D761F30F7B600FF4A0D /* ARDUtilities.m in Sources */, 2CB6ACE92641954700D3D641 /* MapViewController.m in Sources */, 1F8995B32970644C00CABA33 /* ColorGenerator.swift in Sources */, + 1F1B503A2B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift in Sources */, 1F5813F928EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift in Sources */, 2CC32E9227F45AE000BB8C39 /* ReactionsViewCell.swift in Sources */, 2CBF82AE1FC888FC00636459 /* NCPushNotification.m in Sources */, @@ -2633,6 +2646,7 @@ 2C2D7A172B8C9C0000642373 /* RoomCreationTableViewController.swift in Sources */, 1F8995B52973547700CABA33 /* WebRTCCommon.swift in Sources */, 2C4CDCCC269618240023F403 /* RoomDescriptionTableViewCell.m in Sources */, + 1F1B503E2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift in Sources */, DA66583127B6B24E00B46B11 /* UserProfileTableViewController+Utils.swift in Sources */, 2CBF82BD1FD5AE0A00636459 /* NCImageSessionManager.m in Sources */, 1F90EFBC25FE39F800F3FA55 /* NCIntentController.m in Sources */, @@ -2689,13 +2703,12 @@ 2CBF82B21FCC7DBA00636459 /* CCCertificate.m in Sources */, 2CC1FF4828183958009F7288 /* NCDeckCardParameter.m in Sources */, 2C3780BD2107209C003F9AE8 /* NCRoomParticipants.m in Sources */, + 1F5683CF2BA7980C0023E151 /* FilePreviewImageView.swift in Sources */, 1F0B0A722BA264540073FF8D /* MentionSuggestion.swift in Sources */, 2CA1CCCD1F181741002FE6A2 /* NCUser.m in Sources */, 2CBF82B61FD0939600636459 /* NCAPISessionManager.m in Sources */, 1F77A6162AB9B161007B6037 /* ScreenCaptureController.m in Sources */, 2CF8AD3F2A0010FB00A4D3E6 /* MessageTranslationViewController.swift in Sources */, - 2CA155562099E07700CE8EF0 /* GroupedChatMessageTableViewCell.m in Sources */, - 2CA15548208EA1EA00CE8EF0 /* ChatMessageTableViewCell.m in Sources */, 2C4230F72B207AB00013E1FA /* ContextChatViewController.swift in Sources */, 1F90DA0429E9A28E00E81E3D /* AvatarManager.swift in Sources */, 2CC1FF4428147F11009F7288 /* RoomSharedItemsTableViewController.swift in Sources */, @@ -2767,6 +2780,7 @@ 2CB6ACBC26385A3800D3D641 /* ShareLocationViewController.m in Sources */, 1FDCC3D429EBF6E700DEB39B /* AvatarImageView.swift in Sources */, 1FB78E262B6AE5A600B0D69D /* FederationInvitation.swift in Sources */, + 1FDFC94D2BA50B9100670DF4 /* UIFontExtension.swift in Sources */, 1F468E7828DCC7310099597B /* EmojiTextField.swift in Sources */, 80832B762A822E5100195A97 /* UserStatusSwiftUIView.swift in Sources */, 2C444708265E59BC00DF1DBC /* ShareItemController.m in Sources */, @@ -2822,6 +2836,7 @@ 2C4446F9265D5A0700DF1DBC /* NotificationCenterNotifications.m in Sources */, 1F35F8ED2AEEBC1600044BDA /* UIView+SLKAdditions.m in Sources */, 2C1ABD8725769E7D00AEDFB6 /* ShareItemController.m in Sources */, + 1FDFC94F2BA50B9100670DF4 /* UIFontExtension.swift in Sources */, 1F35F8EA2AEEBC0E00044BDA /* SLKTextViewController.m in Sources */, 2C6955132B0CE1A20070F6E1 /* NCUtils.swift in Sources */, 2C62AFFF24C1BDAA007E460A /* NCUser.m in Sources */, @@ -2876,6 +2891,7 @@ 1FB78E222B6ADBB700B0D69D /* NCAPIControllerExtensions.swift in Sources */, 1F1C999D2909846400EACF02 /* BGTaskHelper.swift in Sources */, 1FAB2EF12AD1EAA3001214EB /* RLMSupport.swift in Sources */, + 1FDFC94E2BA50B9100670DF4 /* UIFontExtension.swift in Sources */, 2C4446F4265D51A600DF1DBC /* NCPushNotificationsUtils.m in Sources */, 1FA38C9129A4B3C6008871B8 /* NCNotificationAction.swift in Sources */, 1F1B504D2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */, diff --git a/NextcloudTalk/BaseChatTableViewCell+File.swift b/NextcloudTalk/BaseChatTableViewCell+File.swift new file mode 100644 index 000000000..907f4c536 --- /dev/null +++ b/NextcloudTalk/BaseChatTableViewCell+File.swift @@ -0,0 +1,336 @@ +// +// Copyright (c) 2024 Marcel Müller +// +// Author Marcel Müller +// +// GNU GPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +extension BaseChatTableViewCell { + + func setupForFileCell(with message: NCChatMessage, with account: TalkAccount) { + if self.filePreviewImageView == nil { + // Preview image view + let filePreviewImageView = FilePreviewImageView(frame: .init(x: 0, y: 0, width: fileMessageCellFileMaxPreviewHeight, height: fileMessageCellFileMaxPreviewWidth)) + self.filePreviewImageView = filePreviewImageView + + filePreviewImageView.translatesAutoresizingMaskIntoConstraints = false + filePreviewImageView.layer.cornerRadius = fileMessageCellFilePreviewCornerRadius + filePreviewImageView.layer.masksToBounds = true + filePreviewImageView.contentMode = .scaleAspectFit + + self.messageBodyView.addSubview(filePreviewImageView) + + let previewTap = UITapGestureRecognizer(target: self, action: #selector(filePreviewTapped)) + filePreviewImageView.addGestureRecognizer(previewTap) + filePreviewImageView.isUserInteractionEnabled = true + + // PlayIcon for video files with preview + let filePreviewPlayIconImageView = UIImageView(frame: .init(x: 0, y: 0, width: fileMessageCellFileMaxPreviewHeight, height: fileMessageCellFileMaxPreviewWidth)) + self.filePreviewPlayIconImageView = filePreviewPlayIconImageView + + filePreviewPlayIconImageView.isHidden = true + filePreviewPlayIconImageView.tintColor = .init(white: 1.0, alpha: 0.8) + filePreviewPlayIconImageView.image = .init(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(weight: .black)) + + filePreviewImageView.addSubview(filePreviewPlayIconImageView) + filePreviewImageView.bringSubviewToFront(filePreviewPlayIconImageView) + + // Activity indicator while loading previews + let filePreviewActivityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: fileMessageCellMinimumHeight, height: fileMessageCellMinimumHeight)) + self.filePreviewActivityIndicator = filePreviewActivityIndicator + + filePreviewActivityIndicator.translatesAutoresizingMaskIntoConstraints = false + filePreviewActivityIndicator.radius = fileMessageCellMinimumHeight / 2 + filePreviewActivityIndicator.cycleColors = [.systemGray2] + filePreviewActivityIndicator.indicatorMode = .indeterminate + + filePreviewImageView.addSubview(filePreviewActivityIndicator) + + NSLayoutConstraint.activate([ + filePreviewActivityIndicator.centerYAnchor.constraint(equalTo: filePreviewImageView.centerYAnchor), + filePreviewActivityIndicator.centerXAnchor.constraint(equalTo: filePreviewImageView.centerXAnchor) + ]) + + // Add everything to messageBodyView + let heightConstraint = filePreviewImageView.heightAnchor.constraint(equalToConstant: fileMessageCellFileMaxPreviewHeight) + let widthConstraint = filePreviewImageView.widthAnchor.constraint(equalToConstant: fileMessageCellFileMaxPreviewWidth) + + self.filePreviewImageViewHeightConstraint = heightConstraint + self.filePreviewImageViewWidthConstraint = widthConstraint + + let messageTextView = MessageBodyTextView() + self.messageTextView = messageTextView + + messageTextView.translatesAutoresizingMaskIntoConstraints = false + + self.messageBodyView.addSubview(messageTextView) + + NSLayoutConstraint.activate([ + filePreviewImageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor), + filePreviewImageView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor), + heightConstraint, + widthConstraint, + messageTextView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor), + messageTextView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor), + messageTextView.topAnchor.constraint(equalTo: filePreviewImageView.bottomAnchor, constant: 10), + messageTextView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor) + ]) + + NotificationCenter.default.addObserver(self, selector: #selector(didChangeIsDownloading(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeIsDownloading, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didChangeDownloadProgress(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeDownloadProgress, object: nil) + } + + guard let filePreviewImageView = self.filePreviewImageView, + let messageTextView = self.messageTextView + else { return } + + messageTextView.attributedText = message.parsedMarkdownForChat() + + if message.message == "{file}" { + messageTextView.dataDetectorTypes = [] + } else { + messageTextView.dataDetectorTypes = .all + } + + self.requestPreview(for: message, with: account) + + if !message.sendingFailed { + if message.isTemporary { + self.addActivityIndicator(with: 0) + } else if let fileStatus = message.file().fileStatus { + if fileStatus.isDownloading, fileStatus.downloadProgress < 1 { + self.addActivityIndicator(with: Float(fileStatus.downloadProgress)) + } + } + } + + if let contactImage = message.file().contactPhotoImage() { + filePreviewImageView.image = contactImage + } + } + + func prepareForReuseFileCell() { + self.filePreviewImageView?.cancelImageDownloadTask() + self.filePreviewImageView?.layer.borderWidth = 0 + self.filePreviewImageView?.image = nil + self.filePreviewPlayIconImageView?.isHidden = true + + self.clearFileStatusView() + } + + // MARK: - Preview + + func requestPreview(for message: NCChatMessage, with account: TalkAccount) { + if !message.file().previewAvailable { + // Don't request a preview if we know that there's none + let imageName = "\(NCUtils.previewImage(forMimeType: message.file().mimetype))-chat-preview" + + if let image = UIImage(named: imageName) { + self.filePreviewImageView?.image = image + + self.filePreviewImageViewHeightConstraint?.constant = image.size.height + self.filePreviewImageViewWidthConstraint?.constant = image.size.width + } + + self.filePreviewActivityIndicator?.isHidden = true + self.filePreviewActivityIndicator?.stopAnimating() + + return + } + + let isVideoFile = NCUtils.isVideo(fileType: message.file().mimetype) + let isMediaFile = isVideoFile || NCUtils.isImage(fileType: message.file().mimetype) + + // In case we can determine the height before requesting the preview, adjust the imageView constraints accordingly + if message.file().previewImageHeight > 0 { + self.filePreviewImageViewHeightConstraint?.constant = CGFloat(message.file().previewImageHeight) + } else { + let estimatedPreviewHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message) + + if estimatedPreviewHeight > 0 { + self.filePreviewImageViewHeightConstraint?.constant = estimatedPreviewHeight + } + } + + self.filePreviewActivityIndicator?.isHidden = false + self.filePreviewActivityIndicator?.startAnimating() + + let requestedHeight = Int(3 * fileMessageCellFileMaxPreviewHeight) + guard let previewRequest = NCAPIController.sharedInstance().createPreviewRequest(forFile: message.file().parameterId, withMaxHeight: requestedHeight, using: account) else { return } + + self.filePreviewImageView?.setImageWith(previewRequest, placeholderImage: nil, success: { [weak self] _, _, image in + guard let self, let imageView = self.filePreviewImageView else { return } + + self.filePreviewActivityIndicator?.isHidden = true + self.filePreviewActivityIndicator?.stopAnimating() + + imageView.layer.borderColor = UIColor.secondarySystemFill.cgColor + imageView.layer.borderWidth = 1 + + let imageSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + let previewSize = BaseChatTableViewCell.getPreviewSize(from: imageSize, isMediaFile) + + self.filePreviewImageViewHeightConstraint?.constant = previewSize.height + self.filePreviewImageViewWidthConstraint?.constant = previewSize.width + + if isVideoFile { + // only show the play icon if there is an image preview (not on top of the default video placeholder) + self.filePreviewPlayIconImageView?.isHidden = false + // if the video preview is very narrow, make the play icon fit inside + self.filePreviewPlayIconImageView?.frame = CGRect(x: 0, y: 0, width: min(min(previewSize.height, previewSize.width), fileMessageCellVideoPlayIconSize), height: min(min(previewSize.height, previewSize.width), fileMessageCellVideoPlayIconSize)) + self.filePreviewPlayIconImageView?.center = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0) + } + + imageView.image = image + + self.delegate?.cellHasDownloadedImagePreview(withHeight: ceil(previewSize.height), for: message) + }) + } + + @objc + func filePreviewTapped() { + guard let fileParameter = self.message?.file(), + fileParameter.path != nil, fileParameter.link != nil + else { return } + + self.delegate?.cellWants(toDownloadFile: fileParameter) + } + + // MARK: - Preview height calculation + + static func getPreviewSize(from imageSize: CGSize, _ isMediaFile: Bool) -> CGSize { + var width = imageSize.width + var height = imageSize.height + + let previewMaxHeight = isMediaFile ? fileMessageCellMediaFilePreviewHeight : fileMessageCellFileMaxPreviewHeight + let previewMaxWidth = isMediaFile ? fileMessageCellMediaFileMaxPreviewWidth : fileMessageCellFileMaxPreviewWidth + + if height < fileMessageCellMinimumHeight { + let ratio = fileMessageCellMinimumHeight / height + width *= ratio + + if width > previewMaxWidth { + width = previewMaxWidth + } + + height = fileMessageCellMinimumHeight + } else { + if height > previewMaxHeight { + let ratio = previewMaxHeight / height + width *= ratio + height = previewMaxHeight + } + + if width > previewMaxWidth { + let ratio = previewMaxWidth / width + width = previewMaxWidth + height *= ratio + } + } + + return CGSize(width: width, height: height) + } + + static func getEstimatedPreviewSize(for message: NCChatMessage?) -> CGFloat { + guard let message, let fileParameter = message.file() else { return 0 } + + // We don't have any information about the image to display + if fileParameter.width == 0 && fileParameter.height == 0 { + return 0 + } + + // We can only estimate the height for images and videos + if !NCUtils.isVideo(fileType: fileParameter.mimetype), !NCUtils.isImage(fileType: fileParameter.mimetype) { + return 0 + } + + let imageSize = CGSize(width: CGFloat(fileParameter.width), height: CGFloat(fileParameter.height)) + let previewSize = self.getPreviewSize(from: imageSize, true) + + return ceil(previewSize.height) + } + + // MARK: - File status / activity indicator + + func clearFileStatusView() { + self.fileActivityIndicator?.stopAnimating() + self.fileActivityIndicator?.removeFromSuperview() + self.fileActivityIndicator = nil + } + + func addActivityIndicator(with progress: Float) { + self.clearFileStatusView() + + let fileActivityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20)) + self.fileActivityIndicator = fileActivityIndicator + + fileActivityIndicator.radius = 7 + fileActivityIndicator.cycleColors = [.systemGray2] + + if progress > 0 { + fileActivityIndicator.indicatorMode = .determinate + fileActivityIndicator.setProgress(progress, animated: false) + } + + fileActivityIndicator.startAnimating() + self.statusView.addArrangedSubview(fileActivityIndicator) + } + + // MARK: - File notifications + + @objc func didChangeIsDownloading(notification: Notification) { + DispatchQueue.main.async { + // Make sure this notification is really for this cell + guard let userInfo = notification.userInfo, + let receivedStatus = userInfo["fileStatus"] as? NCChatFileStatus, + let fileParameter = self.message?.file(), + receivedStatus.fileId == fileParameter.parameterId, + receivedStatus.filePath == fileParameter.path, + let isDownloading = userInfo["isDownloading"] as? Bool + else { return } + + if isDownloading, self.fileActivityIndicator == nil { + // Immediately show an indeterminate indicator as long as we don't have a progress value + self.addActivityIndicator(with: 0) + } else if !isDownloading, self.fileActivityIndicator != nil { + self.clearFileStatusView() + } + } + } + + @objc func didChangeDownloadProgress(notification: Notification) { + DispatchQueue.main.async { + // Make sure this notification is really for this cell + guard let userInfo = notification.userInfo, + let receivedStatus = userInfo["fileStatus"] as? NCChatFileStatus, + let fileParameter = self.message?.file(), + receivedStatus.fileId == fileParameter.parameterId, + receivedStatus.filePath == fileParameter.path, + let progress = userInfo["progress"] as? Float + else { return } + + if self.fileActivityIndicator != nil { + // Switch to determinate-mode and show progress + self.fileActivityIndicator?.indicatorMode = .determinate + self.fileActivityIndicator?.setProgress(progress, animated: true) + } else { + // Make sure we have an activity indicator added to this cell + self.addActivityIndicator(with: progress) + } + } + } +} diff --git a/NextcloudTalk/BaseChatTableViewCell+Message.swift b/NextcloudTalk/BaseChatTableViewCell+Message.swift new file mode 100644 index 000000000..5932960db --- /dev/null +++ b/NextcloudTalk/BaseChatTableViewCell+Message.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) 2024 Marcel Müller +// +// Author Marcel Müller +// +// GNU GPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +extension BaseChatTableViewCell { + + func setupForMessageCell(with message: NCChatMessage) { + if self.messageTextView == nil { + let messageTextView = MessageBodyTextView() + self.messageTextView = messageTextView + + messageTextView.translatesAutoresizingMaskIntoConstraints = false + + self.messageBodyView.addSubview(messageTextView) + + NSLayoutConstraint.activate([ + messageTextView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor), + messageTextView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor), + messageTextView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor), + messageTextView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor) + ]) + } + + guard let messageTextView = self.messageTextView else { return } + + messageTextView.attributedText = message.parsedMarkdownForChat() + } + + func prepareForReuseMessageCell() { + self.messageTextView?.text = "" + } +} diff --git a/NextcloudTalk/BaseChatTableViewCell.swift b/NextcloudTalk/BaseChatTableViewCell.swift new file mode 100644 index 000000000..a3cae5253 --- /dev/null +++ b/NextcloudTalk/BaseChatTableViewCell.swift @@ -0,0 +1,493 @@ +// +// Copyright (c) 2024 Marcel Müller +// +// Author Marcel Müller +// +// GNU GPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +protocol BaseChatTableViewCellDelegate: AnyObject { + + func cellWantsToScroll(to message: NCChatMessage) + func cellWantsToReply(to message: NCChatMessage) + func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage) + + func cellWants(toDownloadFile fileParameter: NCMessageFileParameter) + func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage) +} + +// Message cell +public let chatMessageCellIdentifier = "chatMessageCellIdentifier" +public let chatGroupedMessageCellIdentifier = "chatGroupedMessageCellIdentifier" +public let chatReplyMessageCellIdentifier = "chatReplyMessageCellIdentifier" +public let chatMessageCellMinimumHeight = 50.0 +public let chatGroupedMessageCellMinimumHeight = 30.0 + +// File cell +public let fileMessageCellIdentifier = "fileMessageCellIdentifier" +public let fileGroupedMessageCellIdentifier = "fileGroupedMessageCellIdentifier" +public let fileMessageCellMinimumHeight = 50.0 +public let fileMessageCellFileMaxPreviewHeight = 120.0 +public let fileMessageCellFileMaxPreviewWidth = 230.0 +public let fileMessageCellMediaFilePreviewHeight = 230.0 +public let fileMessageCellMediaFileMaxPreviewWidth = 230.0 +public let fileMessageCellFilePreviewCornerRadius = 4.0 +public let fileMessageCellVideoPlayIconSize = 48.0 + +class BaseChatTableViewCell: UITableViewCell, ReactionsViewDelegate { + + public weak var delegate: BaseChatTableViewCellDelegate? + + @IBOutlet weak var avatarButton: AvatarButton! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var dateLabel: UILabel! + @IBOutlet weak var statusView: UIStackView! + @IBOutlet weak var messageBodyView: UIView! + + @IBOutlet weak var headerPart: UIView! + @IBOutlet weak var quotePart: UIView! + @IBOutlet weak var reactionPart: UIView! + @IBOutlet weak var referencePart: UIView! + + public var message: NCChatMessage? + public var messageId: Int = 0 + + internal var quotedMessageView: QuotedMessageView? + internal var reactionView: ReactionsView? + internal var referenceView: ReferenceView? + + internal var replyGestureRecognizer: DRCellSlideGestureRecognizer? + + // Message cell + internal var messageTextView: MessageBodyTextView? + + // File cell + internal var filePreviewImageView: UIImageView? + internal var filePreviewImageViewHeightConstraint: NSLayoutConstraint? + internal var filePreviewImageViewWidthConstraint: NSLayoutConstraint? + internal var fileActivityIndicator: MDCActivityIndicator? + internal var filePreviewActivityIndicator: MDCActivityIndicator? + internal var filePreviewPlayIconImageView: UIImageView? + + override func awakeFromNib() { + super.awakeFromNib() + + self.commonInit() + } + + func commonInit() { + self.headerPart.isHidden = false + self.quotePart.isHidden = true + self.referencePart.isHidden = true + self.reactionPart.isHidden = true + } + + override func prepareForReuse() { + super.prepareForReuse() + + self.message = nil + self.avatarButton.cancelCurrentRequest() + self.avatarButton.setImage(nil, for: .normal) + + self.quotedMessageView?.avatarView.cancelCurrentRequest() + self.quotedMessageView?.avatarView.image = nil + + self.titleLabel.text = "" + self.dateLabel.text = "" + + self.headerPart.isHidden = false + self.quotePart.isHidden = true + self.referencePart.isHidden = true + self.reactionPart.isHidden = true + + self.statusView.isHidden = false + self.statusView.subviews.forEach { $0.removeFromSuperview() } + + self.referenceView?.prepareForReuse() + + self.prepareForReuseMessageCell() + self.prepareForReuseFileCell() + + if let replyGestureRecognizer { + self.removeGestureRecognizer(replyGestureRecognizer) + self.replyGestureRecognizer = nil + } + } + + // swiftlint:disable:next cyclomatic_complexity + public func setup(for message: NCChatMessage, withLastCommonReadMessage lastCommonRead: Int) { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + + self.message = message + self.messageId = message.messageId + + self.avatarButton.setActorAvatar(forMessage: message) + self.avatarButton.menu = self.getDeferredUserMenu() + self.avatarButton.showsMenuAsPrimaryAction = true + + let date = Date(timeIntervalSince1970: TimeInterval(message.timestamp)) + self.dateLabel.text = NCUtils.getTime(fromDate: date) + + var actorDisplayName = message.actorDisplayName ?? "" + + if actorDisplayName.isEmpty { + actorDisplayName = NSLocalizedString("Guest", comment: "") + } + + if let lastEditActorDisplayName = message.lastEditActorDisplayName, message.lastEditTimestamp > 0 { + var editedString = "" + + if message.lastEditActorId == message.actorId, message.lastEditActorType == "users" { + editedString = NSLocalizedString("edited", comment: "A message was edited") + editedString = " (\(editedString))" + } else { + editedString = NSLocalizedString("edited by", comment: "A message was edited by ...") + editedString = " (\(editedString) \(lastEditActorDisplayName)" + } + + let editedAttributedString = editedString.withTextColor(.tertiaryLabel) + let actorDisplayName = actorDisplayName.withTextColor(.secondaryLabel) + + actorDisplayName.append(editedAttributedString) + self.titleLabel.attributedText = actorDisplayName + } else { + self.titleLabel.text = actorDisplayName + } + + let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) + let shouldShowDeliveryStatus = NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityChatReadStatus, forAccountId: activeAccount.accountId) + let shouldShowReadStatus = !serverCapabilities.readStatusPrivacy + + // This check is just a workaround to fix the issue with the deleted parents returned by the API. + let parent = message.parent() + + if let parent { + self.showQuotePart() + + self.quotedMessageView?.actorLabel.text = parent.actorDisplayName.isEmpty ? NSLocalizedString("Guest", comment: "") : parent.actorDisplayName + self.quotedMessageView?.messageLabel.text = parent.parsedMarkdownForChat().string + self.quotedMessageView?.highlighted = parent.isMessage(fromUser: activeAccount.userId) + + self.quotedMessageView?.avatarView.setActorAvatar(forMessage: parent) + } + + if message.isGroupMessage, parent == nil { + self.headerPart.isHidden = true + } + + // When `setDeliveryState` is not called, we still need to make sure the placeholder view is removed + self.statusView.subviews.forEach { $0.removeFromSuperview() } + + if message.isDeleting { + self.setDeliveryState(to: ChatMessageDeliveryStateDeleting) + } else if message.sendingFailed { + self.setDeliveryState(to: ChatMessageDeliveryStateFailed) + } else if message.isTemporary { + self.setDeliveryState(to: ChatMessageDeliveryStateSending) + } else if message.isMessage(fromUser: activeAccount.userId), shouldShowDeliveryStatus { + if lastCommonRead >= message.messageId, shouldShowReadStatus { + self.setDeliveryState(to: ChatMessageDeliveryStateRead) + } else { + self.setDeliveryState(to: ChatMessageDeliveryStateSent) + } + } + + let reactionsArray = message.reactionsArray() + + if !reactionsArray.isEmpty { + self.showReactionsPart() + self.reactionView?.updateReactions(reactions: reactionsArray) + } + + if message.containsURL() { + self.showReferencePart() + + message.getReferenceData { message, referenceDataRaw, url in + guard let message = self.message, + message.isSameMessage(message) + else { return } + + if referenceDataRaw == nil, let deckCard = message.deckCard() { + // In case we were unable to retrieve reference data (for example if the user has no permissions) + // but the message is a shared deck card, we use the shared information to show the deck view + self.referenceView?.update(for: deckCard) + } else if let referenceData = referenceDataRaw as? [String: [String: AnyObject]], let url { + self.referenceView?.update(for: referenceData, and: url) + } + } + } + + if message.isReplyable, !message.isDeleting { + self.addSlideToReplyGestureRecognizer(for: message) + } + + if message.file() != nil { + // File message + self.setupForFileCell(with: message, with: activeAccount) + } else { + // Normal text message + self.setupForMessageCell(with: message) + } + + if message.isDeletedMessage() { + self.statusView.isHidden = true + self.messageTextView?.textColor = .tertiaryLabel + } + } + + func addSlideToReplyGestureRecognizer(for message: NCChatMessage) { + if let action = DRCellSlideAction(forFraction: 0.2) { + action.behavior = .pullBehavior + action.activeColor = .label + action.inactiveColor = .placeholderText + action.activeBackgroundColor = self.backgroundColor + action.inactiveBackgroundColor = self.backgroundColor + action.icon = UIImage(systemName: "arrowshape.turn.up.left") + + action.willTriggerBlock = { [unowned self] _, _ -> Void in + self.delegate?.cellWantsToReply(to: message) + } + + action.didChangeStateBlock = { _, active -> Void in + if active { + // Actuate `Peek` feedback (weak boom) + AudioServicesPlaySystemSound(1519) + } + } + + let replyGestureRecognizer = DRCellSlideGestureRecognizer() + self.replyGestureRecognizer = replyGestureRecognizer + + replyGestureRecognizer.leftActionStartPosition = 80 + replyGestureRecognizer.addActions(action) + + self.addGestureRecognizer(replyGestureRecognizer) + } + } + + func setGuestAvatar(with displayName: String) { + let name = displayName.isEmpty ? "?" : displayName + let image = NCUtils.getImage(withString: name, withBackgroundColor: .placeholderText, withBounds: self.avatarButton.bounds, isCircular: true) + self.avatarButton.setImage(image, for: .normal) + } + + func setBotAvatar() { + let image = NCUtils.getImage(withString: ">", withBackgroundColor: .darkGray, withBounds: self.avatarButton.bounds, isCircular: true) + self.avatarButton.setImage(image, for: .normal) + } + + func setChangelogAvatar() { + self.avatarButton.setImage(UIImage(named: "changelog-avatar"), for: .normal) + } + + func setDeliveryState(to deliveryState: ChatMessageDeliveryState) { + self.statusView.subviews.forEach { $0.removeFromSuperview() } + + if deliveryState == ChatMessageDeliveryStateSending || deliveryState == ChatMessageDeliveryStateDeleting { + let activityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20)) + activityIndicator.radius = 7.0 + activityIndicator.cycleColors = [.lightGray] + activityIndicator.startAnimating() + self.statusView.addArrangedSubview(activityIndicator) + } else if deliveryState == ChatMessageDeliveryStateFailed { + let errorView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) + let errorImage = UIImage(systemName: "exclamationmark.circle")?.withTintColor(.red).withRenderingMode(.alwaysOriginal) + errorView.image = errorImage + self.statusView.addArrangedSubview(errorView) + } else if deliveryState == ChatMessageDeliveryStateSent { + let checkView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) + let checkImage = UIImage(named: "check")?.withRenderingMode(.alwaysTemplate) + checkView.image = checkImage + checkView.contentMode = .scaleAspectFit + checkView.tintColor = .systemGray2 + checkView.accessibilityIdentifier = "MessageSent" + self.statusView.addArrangedSubview(checkView) + } else if deliveryState == ChatMessageDeliveryStateRead { + let checkAllView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) + let checkAllImage = UIImage(named: "check-all")?.withRenderingMode(.alwaysTemplate) + checkAllView.image = checkAllImage + checkAllView.contentMode = .scaleAspectFit + checkAllView.tintColor = .systemGray2 + checkAllView.accessibilityIdentifier = "MessageSent" + self.statusView.addArrangedSubview(checkAllView) + } + } + + // MARK: - QuotePart + + func showQuotePart() { + self.quotePart.isHidden = false + + if self.quotedMessageView == nil { + let quotedMessageView = QuotedMessageView() + self.quotedMessageView = quotedMessageView + + quotedMessageView.translatesAutoresizingMaskIntoConstraints = false + + self.quotePart.addSubview(quotedMessageView) + + NSLayoutConstraint.activate([ + quotedMessageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor), + quotedMessageView.rightAnchor.constraint(equalTo: self.quotePart.rightAnchor, constant: -10), + quotedMessageView.topAnchor.constraint(equalTo: self.quotePart.topAnchor), + quotedMessageView.bottomAnchor.constraint(equalTo: self.quotePart.bottomAnchor) + ]) + + let quoteTap = UITapGestureRecognizer(target: self, action: #selector(quoteTapped(_:))) + quotedMessageView.addGestureRecognizer(quoteTap) + } + } + + @objc func quoteTapped(_ sender: UITapGestureRecognizer?) { + if let message = self.message, let parent = message.parent() { + self.delegate?.cellWantsToScroll(to: parent) + } + } + + // MARK: - ReferencePart + + func showReferencePart() { + self.referencePart.isHidden = false + + if self.referenceView == nil { + let referenceView = ReferenceView() + self.referenceView = referenceView + + referenceView.translatesAutoresizingMaskIntoConstraints = false + + self.referencePart.addSubview(referenceView) + + NSLayoutConstraint.activate([ + referenceView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor), + referenceView.rightAnchor.constraint(equalTo: self.referencePart.rightAnchor, constant: -10), + referenceView.topAnchor.constraint(equalTo: self.referencePart.topAnchor), + referenceView.bottomAnchor.constraint(equalTo: self.referencePart.bottomAnchor, constant: -5) + ]) + } + } + + // MARK: - ReactionsPart + + func showReactionsPart() { + self.reactionPart.isHidden = false + + if self.reactionView == nil { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + + let reactionView = ReactionsView(frame: .init(x: 0, y: 0, width: 50, height: 40), collectionViewLayout: flowLayout) + reactionView.reactionsDelegate = self + self.reactionView = reactionView + + reactionView.translatesAutoresizingMaskIntoConstraints = false + + self.reactionPart.addSubview(reactionView) + + NSLayoutConstraint.activate([ + reactionView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor), + reactionView.rightAnchor.constraint(equalTo: self.reactionPart.rightAnchor, constant: -10), + reactionView.topAnchor.constraint(equalTo: self.reactionPart.topAnchor), + reactionView.bottomAnchor.constraint(equalTo: self.reactionPart.bottomAnchor, constant: -10) + ]) + } + } + + // MARK: - ReactionsView Delegate + + func didSelectReaction(reaction: NCChatReaction) { + if let message = self.message { + self.delegate?.cellDidSelectedReaction(reaction, for: message) + } + } + + // MARK: - Avatar User Menu + + func getDeferredUserMenu() -> UIMenu? { + guard let message = self.message else { return nil } + + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + + if message.actorType != "users" || message.actorId == activeAccount.userId { + return nil + } + + // Use an uncached provider so local time is not cached + let deferredMenuElement = UIDeferredMenuElement.uncached { completion in + self.getMenuUserAction(for: message) { items in + completion(items) + } + } + + return UIMenu(title: message.actorDisplayName, children: [deferredMenuElement]) + } + + func getMenuUserAction(for message: NCChatMessage, completionBlock: @escaping ([UIMenuElement]) -> Void) { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + + NCAPIController.sharedInstance().getUserActions(forUser: message.actorId, using: activeAccount) { userActionsRaw, error in + guard error == nil, + let userActionsDict = userActionsRaw as? [String: AnyObject], + let userActions = userActionsDict["actions"] as? [[String: String]], + let userId = userActionsDict["userId"] as? String + else { + let errorAction = UIAction(title: NSLocalizedString("No actions available", comment: "")) { _ in } + errorAction.attributes = .disabled + completionBlock([errorAction]) + + return + } + + var menuItems: [UIMenuElement] = [] + + for userAction in userActions { + guard let appId = userAction["appId"], + let title = userAction["title"], + let link = userAction["hyperlink"], + let linkEncoded = link.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + else { continue } + + if appId == "spreed" { + let talkAction = UIAction(title: title, image: UIImage(named: "talk-20")?.withRenderingMode(.alwaysTemplate)) { _ in + NotificationCenter.default.post(name: NSNotification.Name.NCChatViewControllerTalkToUserNotification, object: self, userInfo: ["actorId": userId]) + } + + menuItems.append(talkAction) + continue + } + + let otherAction = UIAction(title: title) { _ in + if let actionUrl = URL(string: linkEncoded) { + UIApplication.shared.open(actionUrl) + } + } + + if appId == "profile" { + otherAction.image = UIImage(systemName: "person") + } else if appId == "email" { + otherAction.image = UIImage(systemName: "envelope") + } else if appId == "timezone" { + otherAction.image = UIImage(systemName: "clock") + } else if appId == "social" { + otherAction.image = UIImage(systemName: "heart") + } + + menuItems.append(otherAction) + } + + completionBlock(menuItems) + } + } +} diff --git a/NextcloudTalk/BaseChatTableViewCell.xib b/NextcloudTalk/BaseChatTableViewCell.xib new file mode 100644 index 000000000..87d889834 --- /dev/null +++ b/NextcloudTalk/BaseChatTableViewCell.xib @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NextcloudTalk/BaseChatViewController.swift b/NextcloudTalk/BaseChatViewController.swift index 6a9b12537..75f3f80ae 100644 --- a/NextcloudTalk/BaseChatViewController.swift +++ b/NextcloudTalk/BaseChatViewController.swift @@ -46,10 +46,9 @@ import QuickLook AVAudioPlayerDelegate, SystemMessageTableViewCellDelegate, VoiceMessageTableViewCellDelegate, - FileMessageTableViewCellDelegate, LocationMessageTableViewCellDelegate, ObjectShareMessageTableViewCellDelegate, - ChatMessageTableViewCellDelegate, + BaseChatTableViewCellDelegate, UITableViewDataSourcePrefetching { // MARK: - Internal var @@ -252,11 +251,13 @@ import QuickLook // Set delegate to retrieve typing events self.tableView?.separatorStyle = .none - self.tableView?.register(ChatMessageTableViewCell.self, forCellReuseIdentifier: ChatMessageCellIdentifier) - self.tableView?.register(ChatMessageTableViewCell.self, forCellReuseIdentifier: ReplyMessageCellIdentifier) - self.tableView?.register(GroupedChatMessageTableViewCell.self, forCellReuseIdentifier: GroupedChatMessageCellIdentifier) - self.tableView?.register(FileMessageTableViewCell.self, forCellReuseIdentifier: FileMessageCellIdentifier) - self.tableView?.register(FileMessageTableViewCell.self, forCellReuseIdentifier: GroupedFileMessageCellIdentifier) + self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: chatMessageCellIdentifier) + self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: chatGroupedMessageCellIdentifier) + self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: chatReplyMessageCellIdentifier) + + self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: fileMessageCellIdentifier) + self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: fileGroupedMessageCellIdentifier) + self.tableView?.register(LocationMessageTableViewCell.self, forCellReuseIdentifier: LocationMessageCellIdentifier) self.tableView?.register(LocationMessageTableViewCell.self, forCellReuseIdentifier: GroupedLocationMessageCellIdentifier) self.tableView?.register(SystemMessageTableViewCell.self, forCellReuseIdentifier: SystemMessageCellIdentifier) @@ -1103,8 +1104,8 @@ import QuickLook } func didPressOpenInNextcloud(for message: NCChatMessage) { - if let file = message.file() { - NCUtils.openFileInNextcloudAppOrBrowser(path: file.path, withFileLink: file.link) + if let file = message.file(), let path = file.path, let link = file.link { + NCUtils.openFileInNextcloudAppOrBrowser(path: path, withFileLink: link) } } @@ -2600,9 +2601,9 @@ import QuickLook } if message.file() != nil { - let cellIdentifier = message.isGroupMessage ? GroupedFileMessageCellIdentifier : FileMessageCellIdentifier + let cellIdentifier = message.isGroupMessage ? fileGroupedMessageCellIdentifier : fileMessageCellIdentifier - if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? FileMessageTableViewCell { + if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell { cell.delegate = self cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage) @@ -2632,25 +2633,15 @@ import QuickLook } } - if message.parent() != nil { - if let cell = self.tableView?.dequeueReusableCell(withIdentifier: ReplyMessageCellIdentifier) as? ChatMessageTableViewCell { - cell.delegate = self - cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage) - - return cell - } - } + var cellIdentifier = chatMessageCellIdentifier if message.isGroupMessage { - if let cell = self.tableView?.dequeueReusableCell(withIdentifier: GroupedChatMessageCellIdentifier) as? GroupedChatMessageTableViewCell { - cell.delegate = self - cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage) - - return cell - } + cellIdentifier = chatGroupedMessageCellIdentifier + } else if message.parent() != nil { + cellIdentifier = chatReplyMessageCellIdentifier } - if let cell = self.tableView?.dequeueReusableCell(withIdentifier: ChatMessageCellIdentifier) as? ChatMessageTableViewCell { + if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell { cell.delegate = self cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage) @@ -2669,11 +2660,11 @@ import QuickLook return self.getCellHeight(for: message) } - return kChatMessageCellMinimumHeight + return chatMessageCellMinimumHeight } func getCellHeight(for message: NCChatMessage) -> CGFloat { - guard let tableView = self.tableView else { return kChatMessageCellMinimumHeight } + guard let tableView = self.tableView else { return chatMessageCellMinimumHeight } var width = tableView.frame.width - kChatCellAvatarHeight width -= tableView.safeAreaInsets.left + tableView.safeAreaInsets.right @@ -2721,15 +2712,15 @@ import QuickLook if (message.isGroupMessage && message.parent() == nil) || message.isSystemMessage() { height += 10 // 2*left(5) - if height < kGroupedChatMessageCellMinimumHeight { - height = kGroupedChatMessageCellMinimumHeight + if height < chatGroupedMessageCellMinimumHeight { + height = chatGroupedMessageCellMinimumHeight } } else { height += kChatCellAvatarHeight height += 20.0 // right(10) + 2*left(5) - if height < kChatMessageCellMinimumHeight { - height = kChatMessageCellMinimumHeight + if height < chatMessageCellMinimumHeight { + height = chatMessageCellMinimumHeight } } @@ -2737,13 +2728,11 @@ import QuickLook height += 40 // reactionsView(40) } - // File cells currently can't show the reference view - if message.containsURL(), message.file() == nil { + if message.containsURL() { height += 105 } - // File cells currently can't show a quote, so don't increase the size here - if message.parent() != nil, message.file() == nil { + if message.parent() != nil { height += 65 // left(5) + quoteView(60) } @@ -2751,16 +2740,15 @@ import QuickLook if message.isVoiceMessage() { height -= ceil(bodyBounds.height) height += kVoiceMessageCellPlayerHeight + 10 - } - if let file = message.file() { + } else if let file = message.file() { if file.previewImageHeight > 0 { height += CGFloat(file.previewImageHeight) - } else if case let estimatedHeight = FileMessageTableViewCell.getEstimatedPreviewImageHeight(for: message), estimatedHeight > 0 { + } else if case let estimatedHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message), estimatedHeight > 0 { height += estimatedHeight message.setPreviewImageHeight(estimatedHeight) } else { - height += kFileMessageCellFileMaxPreviewHeight + height += fileMessageCellFileMaxPreviewHeight } height += 10 // right(10) @@ -2809,12 +2797,13 @@ import QuickLook return nil } - if let cell = tableView.cellForRow(at: indexPath) as? ChatTableViewCell { + if let cell = tableView.cellForRow(at: indexPath) as? BaseChatTableViewCell { let pointInCell = tableView.convert(point, to: cell) - let reactionView = cell.contentView.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInCell) }) + let pointInReactionPart = cell.convert(pointInCell, to: cell.reactionPart) + let reactionView = cell.reactionPart.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInReactionPart) }) - if reactionView != nil { - self.showReactionsSummary(of: cell.message) + if reactionView != nil, let message = cell.message { + self.showReactionsSummary(of: message) return nil } } @@ -3171,7 +3160,7 @@ import QuickLook // MARK: - FileMessageTableViewCellDelegate - public func cellWants(toDownloadFile fileParameter: NCMessageFileParameter!) { + public func cellWants(toDownloadFile fileParameter: NCMessageFileParameter) { if fileParameter.fileStatus != nil && fileParameter.fileStatus?.isDownloading ?? false { print("File already downloading -> skipping new download") return @@ -3182,7 +3171,7 @@ import QuickLook downloader.downloadFile(fromMessage: fileParameter) } - public func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage!) { + public func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage) { if message.file().previewImageHeight == Int(height) { return } @@ -3319,7 +3308,7 @@ import QuickLook // MARK: - ChatMessageTableViewCellDelegate - public func cellWantsToScroll(to message: NCChatMessage!) { + public func cellWantsToScroll(to message: NCChatMessage) { DispatchQueue.main.async { if let indexPath = self.indexPath(for: message) { self.highlightMessage(at: indexPath, with: .top) @@ -3327,11 +3316,11 @@ import QuickLook } } - public func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage!) { + public func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage) { // Do nothing -> override in subclass } - public func cellWantsToReply(to message: NCChatMessage!) { + public func cellWantsToReply(to message: NCChatMessage) { if self.textInputbar.isEditing { return } diff --git a/NextcloudTalk/ChatMessageTableViewCell.h b/NextcloudTalk/ChatMessageTableViewCell.h deleted file mode 100644 index 82ac9c413..000000000 --- a/NextcloudTalk/ChatMessageTableViewCell.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Ivan Sein - * - * @author Ivan Sein - * - * @license GNU GPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#import -#import "ChatTableViewCell.h" -#import "NCChatMessage.h" -#import "MessageBodyTextView.h" - -static CGFloat kChatMessageCellMinimumHeight = 50.0; - -static NSString *ChatMessageCellIdentifier = @"ChatMessageCellIdentifier"; -static NSString *ReplyMessageCellIdentifier = @"ReplyMessageCellIdentifier"; - -@class QuotedMessageView; -@class AvatarButton; - -@class ChatMessageTableViewCell; -@protocol ReactionsViewDelegate; - -@protocol ChatMessageTableViewCellDelegate - -- (void)cellWantsToScrollToMessage:(NCChatMessage *)message; - -@end - -@interface ChatMessageTableViewCell : ChatTableViewCell - -@property (nonatomic, weak) id delegate; - -@property (nonatomic, strong) UILabel *titleLabel; -@property (nonatomic, strong) UILabel *dateLabel; -@property (nonatomic, strong) QuotedMessageView *quotedMessageView; -@property (nonatomic, strong) MessageBodyTextView *bodyTextView; -@property (nonatomic, strong) AvatarButton *avatarButton; -@property (nonatomic, strong) UIView *statusView; -@property (nonatomic, strong) UIImageView *userStatusImageView; - -+ (CGFloat)defaultFontSize; -- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead; -- (void)setUserStatus:(NSString *)userStatus; - -@end diff --git a/NextcloudTalk/ChatMessageTableViewCell.m b/NextcloudTalk/ChatMessageTableViewCell.m deleted file mode 100644 index fe8dae9bf..000000000 --- a/NextcloudTalk/ChatMessageTableViewCell.m +++ /dev/null @@ -1,463 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Ivan Sein - * - * @author Ivan Sein - * - * @license GNU GPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#import "ChatMessageTableViewCell.h" - -#import "MaterialActivityIndicator.h" -#import "SLKUIConstants.h" - -#import "NextcloudTalk-Swift.h" - -#import "NCAPIController.h" -#import "NCAppBranding.h" -#import "NCChatMessage.h" -#import "NCDatabaseManager.h" -#import "QuotedMessageView.h" - -@interface ChatMessageTableViewCell () -@property (nonatomic, strong) UIView *quoteContainerView; -@property (nonatomic, strong) ReactionsView *reactionsView; -@property (nonatomic, strong) NSArray *vConstraintNormal; -@property (nonatomic, strong) NSArray *vConstraintReply; -@property (nonatomic, strong) ReferenceView *referenceView; -@end - -@implementation ChatMessageTableViewCell - -- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; - if (self) { - self.backgroundColor = [NCAppBranding backgroundColor]; - [self configureSubviews]; - } - return self; -} - -- (void)configureSubviews -{ - _avatarButton = [[AvatarButton alloc] initWithFrame:CGRectMake(0, 0, kChatCellAvatarHeight, kChatCellAvatarHeight)]; - _avatarButton.translatesAutoresizingMaskIntoConstraints = NO; - _avatarButton.backgroundColor = [NCAppBranding placeholderColor]; - _avatarButton.layer.cornerRadius = kChatCellAvatarHeight/2.0; - _avatarButton.layer.masksToBounds = YES; - _avatarButton.showsMenuAsPrimaryAction = YES; - _avatarButton.imageView.contentMode = UIViewContentModeScaleToFill; - - [self.contentView addSubview:_avatarButton]; - - _statusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kChatCellStatusViewHeight, kChatCellStatusViewHeight)]; - _statusView.translatesAutoresizingMaskIntoConstraints = NO; - [self.contentView addSubview:_statusView]; - - _userStatusImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 12, 12)]; - _userStatusImageView.translatesAutoresizingMaskIntoConstraints = NO; - _userStatusImageView.userInteractionEnabled = NO; - [self.contentView addSubview:_userStatusImageView]; - - [self.contentView addSubview:self.titleLabel]; - [self.contentView addSubview:self.dateLabel]; - [self.contentView addSubview:self.bodyTextView]; - - if ([self.reuseIdentifier isEqualToString:ReplyMessageCellIdentifier]) { - [self.contentView addSubview:self.quoteContainerView]; - [_quoteContainerView addSubview:self.quotedMessageView]; - - UITapGestureRecognizer *quoteTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(quoteTapped:)]; - [self.quoteContainerView addGestureRecognizer:quoteTap]; - } - - [self.contentView addSubview:self.reactionsView]; - [self.contentView addSubview:self.referenceView]; - - NSDictionary *views = @{@"avatarButton": self.avatarButton, - @"userStatusImageView": self.userStatusImageView, - @"statusView": self.statusView, - @"titleLabel": self.titleLabel, - @"dateLabel": self.dateLabel, - @"bodyTextView": self.bodyTextView, - @"quoteContainerView": self.quoteContainerView, - @"quotedMessageView": self.quotedMessageView, - @"reactionsView": self.reactionsView, - @"referenceView": self.referenceView - }; - - NSDictionary *metrics = @{@"avatarSize": @(kChatCellAvatarHeight), - @"dateLabelWidth": @(kChatCellDateLabelWidth), - @"statusSize": @(kChatCellStatusViewHeight), - @"padding": @15, - @"right": @10, - @"left": @5 - }; - - if ([self.reuseIdentifier isEqualToString:ChatMessageCellIdentifier]) { - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[titleLabel]-[dateLabel(>=dateLabelWidth)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[reactionsView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[referenceView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - _vConstraintNormal = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[bodyTextView(>=0@999)]-0-[referenceView(0)]-0-[reactionsView(0)]-(>=left)-|" options:0 metrics:metrics views:views]; - [self.contentView addConstraints:_vConstraintNormal]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[dateLabel(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[statusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; - } else if ([self.reuseIdentifier isEqualToString:ReplyMessageCellIdentifier]) { - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[titleLabel]-[dateLabel(>=dateLabelWidth)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[reactionsView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[referenceView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[quoteContainerView(bodyTextView)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[quotedMessageView(quoteContainerView)]|" options:0 metrics:nil views:views]]; - _vConstraintReply = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[quoteContainerView]-left-[bodyTextView(>=0@999)]-0-[referenceView(0)]-0-[reactionsView(0)]-(>=left)-|" options:0 metrics:metrics views:views]; - [self.contentView addConstraints:_vConstraintReply]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[dateLabel(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[quotedMessageView(quoteContainerView)]|" options:0 metrics:nil views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[quoteContainerView]-left-[statusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; - } - - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[avatarButton(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - - CGFloat pointSize = [ChatMessageTableViewCell defaultFontSize]; - - self.titleLabel.font = [UIFont systemFontOfSize:pointSize]; - self.bodyTextView.font = [UIFont systemFontOfSize:pointSize]; - - self.titleLabel.text = @""; - self.bodyTextView.text = @""; - self.dateLabel.text = @""; - - self.quotedMessageView.actorLabel.text = @""; - self.quotedMessageView.messageLabel.text = @""; - - self.reactionsView.reactions = @[]; - - if (_vConstraintNormal) { - _vConstraintNormal[4].constant = 0; - _vConstraintNormal[5].constant = 0; - _vConstraintNormal[7].constant = 0; - } - - if (_vConstraintReply) { - _vConstraintReply[5].constant = 0; - _vConstraintReply[6].constant = 0; - _vConstraintReply[8].constant = 0; - } - - [_referenceView prepareForReuse]; - - [self.avatarButton cancelCurrentRequest]; - [self.avatarButton setImage:nil forState:UIControlStateNormal]; - - self.userStatusImageView.image = nil; - self.userStatusImageView.backgroundColor = [UIColor clearColor]; - - self.message = nil; - - self.statusView.hidden = NO; - [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; -} - -#pragma mark - Gesture recognizers - -- (void)quoteTapped:(UIGestureRecognizer *)gestureRecognizer -{ - if (self.delegate && self.message && self.message.parent) { - [self.delegate cellWantsToScrollToMessage:self.message.parent]; - } -} - -#pragma mark - ReactionsView delegate - -- (void)didSelectReactionWithReaction:(NCChatReaction *)reaction -{ - [self.delegate cellDidSelectedReaction:reaction forMessage:self.message]; -} - -#pragma mark - Getters - -- (UILabel *)titleLabel -{ - if (!_titleLabel) { - _titleLabel = [UILabel new]; - _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; - _titleLabel.backgroundColor = [UIColor clearColor]; - _titleLabel.userInteractionEnabled = NO; - _titleLabel.numberOfLines = 1; - _titleLabel.font = [UIFont systemFontOfSize:[ChatMessageTableViewCell defaultFontSize]]; - _titleLabel.textColor = [UIColor secondaryLabelColor]; - } - return _titleLabel; -} - -- (UILabel *)dateLabel -{ - if (!_dateLabel) { - _dateLabel = [UILabel new]; - _dateLabel.textAlignment = NSTextAlignmentRight; - _dateLabel.translatesAutoresizingMaskIntoConstraints = NO; - _dateLabel.backgroundColor = [UIColor clearColor]; - _dateLabel.userInteractionEnabled = NO; - _dateLabel.numberOfLines = 1; - _dateLabel.font = [UIFont systemFontOfSize:12.0]; - _dateLabel.textColor = [UIColor secondaryLabelColor]; - } - return _dateLabel; -} - -- (ReactionsView *)reactionsView -{ - if (!_reactionsView) { - UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; - flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; - _reactionsView = [[ReactionsView alloc] initWithFrame:CGRectMake(0, 0, 50, 50) collectionViewLayout:flowLayout]; - _reactionsView.translatesAutoresizingMaskIntoConstraints = NO; - _reactionsView.reactionsDelegate = self; - } - return _reactionsView; -} - -- (ReferenceView *)referenceView -{ - if (!_referenceView) { - _referenceView = [[ReferenceView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; - _referenceView.translatesAutoresizingMaskIntoConstraints = NO; - } - return _referenceView; -} - -- (UIView *)quoteContainerView -{ - if (!_quoteContainerView) { - _quoteContainerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; - _quoteContainerView.translatesAutoresizingMaskIntoConstraints = NO; - } - return _quoteContainerView; -} - - -- (QuotedMessageView *)quotedMessageView -{ - if (!_quotedMessageView) { - _quotedMessageView = [[QuotedMessageView alloc] init]; - _quotedMessageView.translatesAutoresizingMaskIntoConstraints = NO; - } - return _quotedMessageView; -} - -- (MessageBodyTextView *)bodyTextView -{ - if (!_bodyTextView) { - _bodyTextView = [MessageBodyTextView new]; - _bodyTextView.font = [UIFont systemFontOfSize:[ChatMessageTableViewCell defaultFontSize]]; - } - return _bodyTextView; -} - -- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - - if (message.lastEditActorDisplayName || message.lastEditTimestamp > 0) { - NSString *editedString; - - if ([message.lastEditActorId isEqualToString:message.actorId] && [message.lastEditActorType isEqualToString:@"users"]) { - editedString = NSLocalizedString(@"edited", "A message was edited"); - editedString = [NSString stringWithFormat:@" (%@)", editedString]; - } else { - editedString = NSLocalizedString(@"edited by", "A message was edited by ..."); - editedString = [NSString stringWithFormat:@" (%@ %@)", editedString, message.lastEditActorDisplayName]; - } - - NSMutableAttributedString *editedAttributedString = [[NSMutableAttributedString alloc] initWithString:editedString]; - NSRange rangeEditedString = NSMakeRange(0, [editedAttributedString length]); - [editedAttributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:14] range:rangeEditedString]; - [editedAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor tertiaryLabelColor] range:rangeEditedString]; - - NSMutableAttributedString *actorDisplayNameString = [[NSMutableAttributedString alloc] initWithString:message.actorDisplayName]; - [actorDisplayNameString appendAttributedString:editedAttributedString]; - - self.titleLabel.attributedText = actorDisplayNameString; - } else { - self.titleLabel.text = message.actorDisplayName; - } - - self.bodyTextView.attributedText = message.parsedMarkdownForChat; - self.messageId = message.messageId; - self.message = message; - NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp]; - self.dateLabel.text = [NCUtils getTimeFromDate:date]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - BOOL shouldShowDeliveryStatus = [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityChatReadStatus forAccountId:activeAccount.accountId]; - BOOL shouldShowReadStatus = !serverCapabilities.readStatusPrivacy; - - [self.avatarButton setActorAvatarForMessage:message]; - self.avatarButton.menu = [super getDeferredUserMenuForMessage:message]; - - if ([message.actorType isEqualToString:@"guests"]) { - self.titleLabel.text = ([message.actorDisplayName isEqualToString:@""]) ? NSLocalizedString(@"Guest", nil) : message.actorDisplayName; - } - - // This check is just a workaround to fix the issue with the deleted parents returned by the API. - NCChatMessage *parent = message.parent; - if (parent.message) { - self.quotedMessageView.actorLabel.text = ([parent.actorDisplayName isEqualToString:@""]) ? NSLocalizedString(@"Guest", nil) : parent.actorDisplayName; - self.quotedMessageView.messageLabel.text = parent.parsedMarkdownForChat.string; - self.quotedMessageView.highlighted = [parent isMessageFromUser:activeAccount.userId]; - [self.quotedMessageView.avatarView setActorAvatarForMessage:parent]; - } - - if (message.isDeleting) { - [self setDeliveryState:ChatMessageDeliveryStateDeleting]; - } else if (message.sendingFailed) { - [self setDeliveryState:ChatMessageDeliveryStateFailed]; - } else if (message.isTemporary){ - [self setDeliveryState:ChatMessageDeliveryStateSending]; - } else if ([message isMessageFromUser:activeAccount.userId] && shouldShowDeliveryStatus) { - if (lastCommonRead >= message.messageId && shouldShowReadStatus) { - [self setDeliveryState:ChatMessageDeliveryStateRead]; - } else { - [self setDeliveryState:ChatMessageDeliveryStateSent]; - } - } - - if (message.isDeletedMessage) { - self.statusView.hidden = YES; - self.bodyTextView.textColor = [UIColor tertiaryLabelColor]; - } - - [self.reactionsView updateReactionsWithReactions:message.reactionsArray]; - if (message.reactionsArray.count > 0) { - if (_vConstraintNormal) { - _vConstraintNormal[7].constant = 40; - } - - if (_vConstraintReply) { - _vConstraintReply[8].constant = 40; - } - } - - if (message.containsURL) { - if (_vConstraintNormal) { - _vConstraintNormal[4].constant = 5; - _vConstraintNormal[5].constant = 100; - } - - if (_vConstraintReply) { - _vConstraintReply[5].constant = 5; - _vConstraintReply[6].constant = 100; - } - - [message getReferenceDataWithCompletionBlock:^(NCChatMessage *message, NSDictionary *referenceData, NSString *url) { - if (![self.message isSameMessage:message]) { - return; - } - - if (!referenceData && message.deckCard) { - // In case we were unable to retrieve reference data (for example if the user has no permissions) - // but the message is a shared deck card, we use the shared information to show the deck view - [self.referenceView updateFor:message.deckCard]; - } else { - [self.referenceView updateFor:referenceData and:url]; - } - }]; - } - - if (self.message.isReplyable && !self.message.isDeleting) { - __weak typeof(self) weakSelf = self; - [self addReplyGestureWithActionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { - __strong typeof(self) strongSelf = weakSelf; - [strongSelf.delegate cellWantsToReplyToMessage:strongSelf.message]; - }]; - } -} - -- (void)setDeliveryState:(ChatMessageDeliveryState)state -{ - [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; - - if (state == ChatMessageDeliveryStateSending || state == ChatMessageDeliveryStateDeleting) { - MDCActivityIndicator *activityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - activityIndicator.radius = 7.0f; - activityIndicator.cycleColors = @[UIColor.lightGrayColor]; - [activityIndicator startAnimating]; - [self.statusView addSubview:activityIndicator]; - } else if (state == ChatMessageDeliveryStateFailed) { - UIImageView *errorView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [errorView setImage:[UIImage imageNamed:@"error"]]; - [self.statusView addSubview:errorView]; - } else if (state == ChatMessageDeliveryStateSent) { - UIImageView *checkView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [checkView setImage:[UIImage imageNamed:@"check"]]; - checkView.image = [checkView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - [checkView setTintColor:[UIColor lightGrayColor]]; - [checkView setAccessibilityValue:@"MessageSent"]; - [self.statusView addSubview:checkView]; - } else if (state == ChatMessageDeliveryStateRead) { - UIImageView *checkAllView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [checkAllView setImage:[UIImage imageNamed:@"check-all"]]; - checkAllView.image = [checkAllView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - [checkAllView setTintColor:[UIColor lightGrayColor]]; - [checkAllView setAccessibilityValue:@"MessageSent"]; - [self.statusView addSubview:checkAllView]; - } -} - -- (void)setUserStatus:(NSString *)userStatus -{ - UIImage *statusImage = nil; - if ([userStatus isEqualToString:@"online"]) { - statusImage = [UIImage imageNamed:@"user-status-online-10"]; - } else if ([userStatus isEqualToString:@"away"]) { - statusImage = [UIImage imageNamed:@"user-status-away-10"]; - } else if ([userStatus isEqualToString:@"dnd"]) { - statusImage = [UIImage imageNamed:@"user-status-dnd-10"]; - } - - if (statusImage) { - [_userStatusImageView setImage:statusImage]; - _userStatusImageView.contentMode = UIViewContentModeCenter; - _userStatusImageView.layer.cornerRadius = 6; - _userStatusImageView.clipsToBounds = YES; - - // When a background color is set directly to the cell it seems that there is no background configuration. - // In this class, even when no background color is set, the background configuration is nil. - _userStatusImageView.backgroundColor = (self.backgroundColor) ? self.backgroundColor : [[self backgroundConfiguration] backgroundColor]; - } -} - -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - -// NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; -// pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - - -@end diff --git a/NextcloudTalk/ChatViewController.swift b/NextcloudTalk/ChatViewController.swift index cee25cf4a..df8a232bd 100644 --- a/NextcloudTalk/ChatViewController.swift +++ b/NextcloudTalk/ChatViewController.swift @@ -1529,12 +1529,13 @@ import UIKit return nil } - if let cell = tableView.cellForRow(at: indexPath) as? ChatTableViewCell { + if let cell = tableView.cellForRow(at: indexPath) as? BaseChatTableViewCell { let pointInCell = tableView.convert(point, to: cell) - let reactionView = cell.contentView.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInCell) }) + let pointInReactionPart = cell.convert(pointInCell, to: cell.reactionPart) + let reactionView = cell.reactionPart.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInReactionPart) }) - if reactionView != nil { - self.showReactionsSummary(of: cell.message) + if reactionView != nil, let message = cell.message { + self.showReactionsSummary(of: message) return nil } } diff --git a/NextcloudTalk/FileMessageTableViewCell.h b/NextcloudTalk/FileMessageTableViewCell.h deleted file mode 100644 index 8eb8fa519..000000000 --- a/NextcloudTalk/FileMessageTableViewCell.h +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Ivan Sein - * - * @author Ivan Sein - * - * @license GNU GPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#import -#import "ChatTableViewCell.h" -#import "MessageBodyTextView.h" -#import "NCMessageFileParameter.h" -#import "NCChatMessage.h" - -static CGFloat kFileMessageCellMinimumHeight = 50.0; -static CGFloat kFileMessageCellFileMaxPreviewHeight = 120.0; -static CGFloat kFileMessageCellFileMaxPreviewWidth = 230.0; -static CGFloat kFileMessageCellMediaFilePreviewHeight = 230.0; -static CGFloat kFileMessageCellMediaFileMaxPreviewWidth = 230.0; -static CGFloat kFileMessageCellFilePreviewCornerRadius = 4.0; -static CGFloat kFileMessageCellVideoPlayIconSize = 48.0; - -static NSString *FileMessageCellIdentifier = @"FileMessageCellIdentifier"; -static NSString *GroupedFileMessageCellIdentifier = @"GroupedFileMessageCellIdentifier"; - -@interface FilePreviewImageView : UIImageView -@end - -@class AvatarButton; -@class FileMessageTableViewCell; -@class ReactionsView; -@protocol ReactionsViewDelegate; - -@protocol FileMessageTableViewCellDelegate - -- (void)cellWantsToDownloadFile:(NCMessageFileParameter *)fileParameter; -- (void)cellHasDownloadedImagePreviewWithHeight:(CGFloat)height forMessage:(NCChatMessage *)message; - -@end - -@interface FileMessageTableViewCell : ChatTableViewCell - -@property (nonatomic, weak) id delegate; - -@property (nonatomic, strong) UILabel *titleLabel; -@property (nonatomic, strong) UILabel *dateLabel; -@property (nonatomic, strong) FilePreviewImageView *previewImageView; -@property (nonatomic, strong) UIImageView *playIconImageView; -@property (nonatomic, strong) MessageBodyTextView *bodyTextView; -@property (nonatomic, strong) AvatarButton *avatarButton; -@property (nonatomic, strong) UIView *statusView; -@property (nonatomic, strong) UIView *fileStatusView; -@property (nonatomic, strong) UIStackView *statusStackView; -@property (nonatomic, strong) NCMessageFileParameter *fileParameter; - -@property (nonatomic, strong) ReactionsView *reactionsView; -@property (nonatomic, strong) NSArray *vPreviewSize; -@property (nonatomic, strong) NSArray *hPreviewSize; -@property (nonatomic, strong) NSArray *vGroupedPreviewSize; -@property (nonatomic, strong) NSArray *hGroupedPreviewSize; - -+ (CGFloat)defaultFontSize; -+ (CGFloat)getEstimatedPreviewImageHeightForMessage:(NCChatMessage *)message; -- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead; - -@end diff --git a/NextcloudTalk/FileMessageTableViewCell.m b/NextcloudTalk/FileMessageTableViewCell.m deleted file mode 100644 index 2745d6085..000000000 --- a/NextcloudTalk/FileMessageTableViewCell.m +++ /dev/null @@ -1,604 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Ivan Sein - * - * @author Ivan Sein - * - * @license GNU GPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#import "FileMessageTableViewCell.h" - -#import "MaterialActivityIndicator.h" -#import "SLKUIConstants.h" - -#import "NCAPIController.h" -#import "NCAppBranding.h" -#import "NCChatFileController.h" -#import "NCDatabaseManager.h" - -#import "NextcloudTalk-Swift.h" - -@implementation FilePreviewImageView : UIImageView - -@end - -@interface FileMessageTableViewCell () -{ - MDCActivityIndicator *_activityIndicator; - MDCActivityIndicator *_previewActivityIndicator; -} - -@end - -@implementation FileMessageTableViewCell - -- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; - if (self) { - self.backgroundColor = [NCAppBranding backgroundColor]; - [self configureSubviews]; - } - return self; -} - -- (void)configureSubviews -{ - _avatarButton = [[AvatarButton alloc] initWithFrame:CGRectMake(0, 0, kChatCellAvatarHeight, kChatCellAvatarHeight)]; - _avatarButton.translatesAutoresizingMaskIntoConstraints = NO; - _avatarButton.backgroundColor = [NCAppBranding placeholderColor]; - _avatarButton.layer.cornerRadius = kChatCellAvatarHeight/2.0; - _avatarButton.layer.masksToBounds = YES; - _avatarButton.showsMenuAsPrimaryAction = YES; - _avatarButton.imageView.contentMode = UIViewContentModeScaleToFill; - - _playIconImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, kFileMessageCellVideoPlayIconSize, kFileMessageCellVideoPlayIconSize)]; - _playIconImageView.hidden = YES; - [_playIconImageView setTintColor:[UIColor colorWithWhite:1.0 alpha:0.8]]; - [_playIconImageView setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightBlack]]]; - - _previewImageView = [[FilePreviewImageView alloc] initWithFrame:CGRectMake(0, 0, kFileMessageCellFileMaxPreviewHeight, kFileMessageCellFileMaxPreviewHeight)]; - _previewImageView.translatesAutoresizingMaskIntoConstraints = NO; - _previewImageView.userInteractionEnabled = NO; - _previewImageView.layer.cornerRadius = kFileMessageCellFilePreviewCornerRadius; - _previewImageView.layer.masksToBounds = YES; - [_previewImageView addSubview:_playIconImageView]; - [_previewImageView bringSubviewToFront:_playIconImageView]; - - UITapGestureRecognizer *previewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(previewTapped:)]; - [_previewImageView addGestureRecognizer:previewTap]; - _previewImageView.userInteractionEnabled = YES; - - _previewActivityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectMake(0, 0, kFileMessageCellMinimumHeight, kFileMessageCellMinimumHeight)]; - _previewActivityIndicator.translatesAutoresizingMaskIntoConstraints = NO; - _previewActivityIndicator.radius = kFileMessageCellMinimumHeight / 2; - _previewActivityIndicator.cycleColors = @[UIColor.lightGrayColor]; - _previewActivityIndicator.indicatorMode = MDCActivityIndicatorModeIndeterminate; - - if ([self.reuseIdentifier isEqualToString:FileMessageCellIdentifier]) { - [self.contentView addSubview:self.avatarButton]; - [self.contentView addSubview:self.titleLabel]; - [self.contentView addSubview:self.dateLabel]; - } - - [self.contentView addSubview:self.bodyTextView]; - [self.contentView addSubview:_previewImageView]; - [_previewImageView addSubview:_previewActivityIndicator]; - - _statusStackView = [[UIStackView alloc] init]; - _statusStackView.translatesAutoresizingMaskIntoConstraints = NO; - _statusStackView.axis = UILayoutConstraintAxisVertical; - _statusStackView.distribution = UIStackViewDistributionEqualSpacing; - _statusStackView.alignment = UIStackViewAlignmentTop; - [self.contentView addSubview:self.statusStackView]; - - _statusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kChatCellStatusViewHeight, kChatCellStatusViewHeight)]; - _statusView.translatesAutoresizingMaskIntoConstraints = NO; - [self.statusStackView addArrangedSubview:_statusView]; - - _fileStatusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kChatCellStatusViewHeight, kChatCellStatusViewHeight)]; - _fileStatusView.translatesAutoresizingMaskIntoConstraints = NO; - [self.statusStackView addArrangedSubview:_fileStatusView]; - - [self.contentView addSubview:self.reactionsView]; - - _previewImageView.contentMode = UIViewContentModeScaleAspectFit; - - NSDictionary *views = @{@"avatarButton": self.avatarButton, - @"statusStackView": self.statusStackView, - @"titleLabel": self.titleLabel, - @"dateLabel": self.dateLabel, - @"previewImageView": self.previewImageView, - @"bodyTextView": self.bodyTextView, - @"reactionsView": self.reactionsView - }; - - NSDictionary *metrics = @{@"avatarSize": @(kChatCellAvatarHeight), - @"dateLabelWidth": @(kChatCellDateLabelWidth), - @"previewSize": @(kFileMessageCellFileMaxPreviewHeight), - @"statusStackHeight" : @(kChatCellStatusViewHeight), - @"padding": @15, - @"avatarGap": @50, - @"right": @10, - @"left": @5 - }; - - if ([self.reuseIdentifier isEqualToString:FileMessageCellIdentifier]) { - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[titleLabel]-[dateLabel(>=dateLabelWidth)]-right-|" options:0 metrics:metrics views:views]]; - self.hPreviewSize = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[previewImageView(previewSize)]-(>=0)-|" options:0 metrics:metrics views:views]; - [self.contentView addConstraints:self.hPreviewSize]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[reactionsView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[dateLabel(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; - self.vPreviewSize = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[previewImageView(previewSize)]-right-[bodyTextView(>=0@999)]-0-[reactionsView(0)]-left-|" options:0 metrics:metrics views:views]; - [self.contentView addConstraints:self.vPreviewSize]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[avatarButton(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[statusStackView(statusStackHeight)]-(>=0)-|" options:0 metrics:metrics views:views]]; - } else if ([self.reuseIdentifier isEqualToString:GroupedFileMessageCellIdentifier]) { - self.hGroupedPreviewSize = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-avatarGap-[previewImageView(previewSize)]-(>=0)-|" options:0 metrics:metrics views:views]; - [self.contentView addConstraints:self.hGroupedPreviewSize]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-avatarGap-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-avatarGap-[reactionsView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - self.vGroupedPreviewSize = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-left-[previewImageView(previewSize)]-right-[bodyTextView(>=0@999)]-0-[reactionsView(0)]-left-|" options:0 metrics:metrics views:views]; - [self.contentView addConstraints:self.vGroupedPreviewSize]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-left-[statusStackView(statusStackHeight)]-(>=0)-|" options:0 metrics:metrics views:views]]; - } - - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusStackView(statusStackHeight)]-padding-[previewImageView(>=0)]-(>=0)-|" options:NSLayoutFormatAlignAllTop metrics:metrics views:views]]; - - [NSLayoutConstraint activateConstraints:@[[_previewActivityIndicator.centerYAnchor constraintEqualToAnchor:_previewImageView.centerYAnchor]]]; - [NSLayoutConstraint activateConstraints:@[[_previewActivityIndicator.centerXAnchor constraintEqualToAnchor:_previewImageView.centerXAnchor]]]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeIsDownloading:) name:NCChatFileControllerDidChangeIsDownloadingNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeDownloadProgress:) name:NCChatFileControllerDidChangeDownloadProgressNotification object:nil]; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - - CGFloat pointSize = [FileMessageTableViewCell defaultFontSize]; - - self.titleLabel.font = [UIFont systemFontOfSize:pointSize]; - self.bodyTextView.font = [UIFont systemFontOfSize:pointSize]; - - self.titleLabel.text = @""; - self.bodyTextView.text = @""; - self.dateLabel.text = @""; - - [self.avatarButton cancelCurrentRequest]; - [self.avatarButton setImage:nil forState:UIControlStateNormal]; - - [self.previewImageView cancelImageDownloadTask]; - self.previewImageView.layer.borderWidth = 0.0f; - self.previewImageView.image = nil; - self.playIconImageView.hidden = YES; - - self.vPreviewSize[3].constant = kFileMessageCellFileMaxPreviewHeight; - self.hPreviewSize[3].constant = kFileMessageCellFileMaxPreviewHeight; - self.vGroupedPreviewSize[1].constant = kFileMessageCellFileMaxPreviewHeight; - self.hGroupedPreviewSize[1].constant = kFileMessageCellFileMaxPreviewHeight; - - self.vPreviewSize[7].constant = 0; - self.vGroupedPreviewSize[5].constant = 0; - - [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; - [self clearFileStatusView]; -} - -- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection -{ - if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) { - // We use a CGColor so we loose the automatic color changing of dynamic colors -> update manually - self.previewImageView.layer.borderColor = [[UIColor secondarySystemFillColor] CGColor]; - } -} - -- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - - if (message.lastEditActorDisplayName || message.lastEditTimestamp > 0) { - NSString *editedString; - - if ([message.lastEditActorId isEqualToString:message.actorId] && [message.lastEditActorType isEqualToString:@"users"]) { - editedString = NSLocalizedString(@"edited", "A message was edited"); - editedString = [NSString stringWithFormat:@" (%@)", editedString]; - } else { - editedString = NSLocalizedString(@"edited by", "A message was edited by ..."); - editedString = [NSString stringWithFormat:@" (%@ %@)", editedString, message.lastEditActorDisplayName]; - } - - NSMutableAttributedString *editedAttributedString = [[NSMutableAttributedString alloc] initWithString:editedString]; - NSRange rangeEditedString = NSMakeRange(0, [editedAttributedString length]); - [editedAttributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:14] range:rangeEditedString]; - [editedAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor tertiaryLabelColor] range:rangeEditedString]; - - NSMutableAttributedString *actorDisplayNameString = [[NSMutableAttributedString alloc] initWithString:message.actorDisplayName]; - [actorDisplayNameString appendAttributedString:editedAttributedString]; - - self.titleLabel.attributedText = actorDisplayNameString; - } else { - self.titleLabel.text = message.actorDisplayName; - } - - self.bodyTextView.attributedText = message.parsedMarkdownForChat; - self.messageId = message.messageId; - self.message = message; - - if ([message.message isEqualToString:@"{file}"]) { - self.bodyTextView.dataDetectorTypes = UIDataDetectorTypeNone; - } else { - self.bodyTextView.dataDetectorTypes = UIDataDetectorTypeAll; - } - - NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp]; - self.dateLabel.text = [NCUtils getTimeFromDate:date]; - - [self.avatarButton setActorAvatarForMessage:message]; - _avatarButton.menu = [super getDeferredUserMenuForMessage:message]; - - [self requestPreviewForMessage:message withAccount:activeAccount]; - - if (message.sendingFailed) { - UIImageView *errorView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [errorView setImage:[UIImage imageNamed:@"error"]]; - [self.statusView addSubview:errorView]; - } else if (message.isTemporary) { - [self addActivityIndicator:0]; - } else if (message.file.fileStatus) { - if (message.file.fileStatus.isDownloading && message.file.fileStatus.downloadProgress < 1) { - [self addActivityIndicator:message.file.fileStatus.downloadProgress]; - } - } - - self.fileParameter = message.file; - - if (message.file.contactPhotoImage) { - [self.previewImageView setImage:message.file.contactPhotoImage]; - } - - [self.reactionsView updateReactionsWithReactions:message.reactionsArray]; - if (message.reactionsArray.count > 0) { - _vPreviewSize[7].constant = 40; - _vGroupedPreviewSize[5].constant = 40; - } - - if ([message.actorId isEqualToString:activeAccount.userId]) { - [self.statusView setHidden:NO]; - } else { - [self.statusView setHidden:YES]; - } - - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - BOOL shouldShowDeliveryStatus = [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityChatReadStatus forAccountId:activeAccount.accountId]; - BOOL shouldShowReadStatus = !serverCapabilities.readStatusPrivacy; - if ([message.actorId isEqualToString:activeAccount.userId] && [message.actorType isEqualToString:@"users"] && shouldShowDeliveryStatus) { - if (lastCommonRead >= message.messageId && shouldShowReadStatus) { - [self setDeliveryState:ChatMessageDeliveryStateRead]; - } else { - [self setDeliveryState:ChatMessageDeliveryStateSent]; - } - } - - if (self.message.isReplyable && !self.message.isDeleting) { - __weak typeof(self) weakSelf = self; - [self addReplyGestureWithActionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { - __strong typeof(self) strongSelf = weakSelf; - [strongSelf.delegate cellWantsToReplyToMessage:strongSelf.message]; - }]; - } -} - -- (void)requestPreviewForMessage:(NCChatMessage *)message withAccount:(TalkAccount *)account -{ - if (!message.file.previewAvailable) { - // Don't request a preview if we know that there's none - NSString *imageName = [[NCUtils previewImageForMimeType:message.file.mimetype] stringByAppendingString:@"-chat-preview"]; - [self.previewImageView setImage:[UIImage imageNamed:imageName]]; - - [_previewActivityIndicator setHidden:YES]; - [_previewActivityIndicator stopAnimating]; - - return; - } - - BOOL isVideoFile = [NCUtils isVideoWithFileType:message.file.mimetype]; - BOOL isMediaFile = isVideoFile || [NCUtils isImageWithFileType:message.file.mimetype]; - - NSInteger requestedHeight = 3 * kFileMessageCellFileMaxPreviewHeight; - __weak typeof(self) weakSelf = self; - - // In case we can determine the height before requesting the preview, adjust the imageView constraints accordingly - if (message.file.previewImageHeight > 0) { - self.vPreviewSize[3].constant = message.file.previewImageHeight; - self.vGroupedPreviewSize[1].constant = message.file.previewImageHeight; - } else { - CGFloat estimatedPreviewHeight = [FileMessageTableViewCell getEstimatedPreviewImageHeightForMessage:message]; - - if (estimatedPreviewHeight > 0) { - self.vPreviewSize[3].constant = estimatedPreviewHeight; - self.vGroupedPreviewSize[1].constant = estimatedPreviewHeight; - } - } - - [_previewActivityIndicator setHidden:NO]; - [_previewActivityIndicator startAnimating]; - - [self.previewImageView setImageWithURLRequest:[[NCAPIController sharedInstance] createPreviewRequestForFile:message.file.parameterId withMaxHeight:requestedHeight usingAccount:account] - placeholderImage:nil success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull image) { - - __strong typeof(self) strongSelf = weakSelf; - - if (strongSelf) { - [strongSelf->_previewActivityIndicator setHidden:YES]; - [strongSelf->_previewActivityIndicator stopAnimating]; - } - - weakSelf.previewImageView.layer.borderColor = [[UIColor secondarySystemFillColor] CGColor]; - weakSelf.previewImageView.layer.borderWidth = 1.0f; - - CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale); - CGSize previewSize = [FileMessageTableViewCell getPreviewSizeFromImageSize:imageSize isMediaFile:isMediaFile]; - - weakSelf.vPreviewSize[3].constant = previewSize.height; - weakSelf.hPreviewSize[3].constant = previewSize.width; - weakSelf.vGroupedPreviewSize[1].constant = previewSize.height; - weakSelf.hGroupedPreviewSize[1].constant = previewSize.width; - - if (isVideoFile) { - // only show the play icon if there is an image preview (not on top of the default video placeholder) - weakSelf.playIconImageView.hidden = NO; - // if the video preview is very narrow, make the play icon fit inside - weakSelf.playIconImageView.frame = CGRectMake(0, 0, MIN(MIN(previewSize.height, previewSize.width), kFileMessageCellVideoPlayIconSize), MIN(MIN(previewSize.height, previewSize.width), kFileMessageCellVideoPlayIconSize)); - weakSelf.playIconImageView.center = CGPointMake(previewSize.width / 2.0, previewSize.height / 2.0); - } - - [weakSelf.previewImageView setImage:image]; - - if (weakSelf.delegate) { - [weakSelf.delegate cellHasDownloadedImagePreviewWithHeight:ceil(previewSize.height) forMessage:message]; - } - } failure:nil]; -} - -+ (CGSize)getPreviewSizeFromImageSize:(CGSize)imageSize isMediaFile:(BOOL)isMediaFile { - CGFloat width = imageSize.width; - CGFloat height = imageSize.height; - - CGFloat previewMaxHeight = isMediaFile ? kFileMessageCellMediaFilePreviewHeight : kFileMessageCellFileMaxPreviewHeight; - CGFloat previewMaxWidth = isMediaFile ? kFileMessageCellMediaFileMaxPreviewWidth : kFileMessageCellFileMaxPreviewWidth; - - if (height < kFileMessageCellMinimumHeight) { - CGFloat ratio = kFileMessageCellMinimumHeight / height; - width = width * ratio; - if (width > previewMaxWidth) { - width = previewMaxWidth; - } - height = kFileMessageCellMinimumHeight; - } else { - if (height > previewMaxHeight) { - CGFloat ratio = previewMaxHeight / height; - width = width * ratio; - height = previewMaxHeight; - } - if (width > previewMaxWidth) { - CGFloat ratio = previewMaxWidth / width; - width = previewMaxWidth; - height = height * ratio; - } - } - - return CGSizeMake(width, height); -} - -+ (CGFloat)getEstimatedPreviewImageHeightForMessage:(NCChatMessage *)message -{ - if (!message || !message.file) { - return 0; - } - - NCMessageFileParameter *fileParameter = message.file; - - // We don't have any information about the image to display - if (fileParameter.width == 0 && fileParameter.height == 0) { - return 0; - } - - // We can only estimate the height for images and videos - if (![NCUtils isVideoWithFileType:fileParameter.mimetype] && ![NCUtils isImageWithFileType:fileParameter.mimetype]) { - return 0; - } - - CGSize imageSize = CGSizeMake(fileParameter.width, fileParameter.height); - CGSize previewSize = [self getPreviewSizeFromImageSize:imageSize isMediaFile:YES]; - - return ceil(previewSize.height); -} - -- (void)setDeliveryState:(ChatMessageDeliveryState)state -{ - [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; - - if (state == ChatMessageDeliveryStateSent) { - UIImageView *checkView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [checkView setImage:[UIImage imageNamed:@"check"]]; - checkView.image = [checkView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - [checkView setTintColor:[UIColor lightGrayColor]]; - [self.statusView addSubview:checkView]; - } else if (state == ChatMessageDeliveryStateRead) { - UIImageView *checkAllView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [checkAllView setImage:[UIImage imageNamed:@"check-all"]]; - checkAllView.image = [checkAllView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - [checkAllView setTintColor:[UIColor lightGrayColor]]; - [self.statusView addSubview:checkAllView]; - } -} - -- (void)didChangeIsDownloading:(NSNotification *)notification -{ - dispatch_async(dispatch_get_main_queue(), ^{ - NCChatFileStatus *receivedStatus = [notification.userInfo objectForKey:@"fileStatus"]; - - if (![receivedStatus.fileId isEqualToString:self->_fileParameter.parameterId] || ![receivedStatus.filePath isEqualToString:self->_fileParameter.path]) { - // Received a notification for a different cell - return; - } - - BOOL isDownloading = [[notification.userInfo objectForKey:@"isDownloading"] boolValue]; - - if (isDownloading && !self->_activityIndicator) { - // Immediately show an indeterminate indicator as long as we don't have a progress value - [self addActivityIndicator:0]; - } else if (!isDownloading && self->_activityIndicator) { - [self clearFileStatusView]; - } - }); -} -- (void)didChangeDownloadProgress:(NSNotification *)notification -{ - dispatch_async(dispatch_get_main_queue(), ^{ - NCChatFileStatus *receivedStatus = [notification.userInfo objectForKey:@"fileStatus"]; - - if (![receivedStatus.fileId isEqualToString:self->_fileParameter.parameterId] || ![receivedStatus.filePath isEqualToString:self->_fileParameter.path]) { - // Received a notification for a different cell - return; - } - - double progress = [[notification.userInfo objectForKey:@"progress"] doubleValue]; - - if (self->_activityIndicator) { - // Switch to determinate-mode and show progress - self->_activityIndicator.indicatorMode = MDCActivityIndicatorModeDeterminate; - [self->_activityIndicator setProgress:progress animated:YES]; - } else { - // Make sure we have an activity indicator added to this cell - [self addActivityIndicator:progress]; - } - }); -} - -- (void)addActivityIndicator:(CGFloat)progress -{ - [self clearFileStatusView]; - - _activityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - _activityIndicator.radius = 7.0f; - _activityIndicator.cycleColors = @[UIColor.lightGrayColor]; - - if (progress > 0) { - _activityIndicator.indicatorMode = MDCActivityIndicatorModeDeterminate; - [_activityIndicator setProgress:progress animated:NO]; - } - - [_activityIndicator startAnimating]; - [self.fileStatusView addSubview:_activityIndicator]; -} - -#pragma mark - Gesture recognizers - -- (void)previewTapped:(UITapGestureRecognizer *)recognizer -{ - if (!self.fileParameter || !self.fileParameter.path || !self.fileParameter.link) { - return; - } - - if (self.delegate) { - [self.delegate cellWantsToDownloadFile:self.fileParameter]; - } -} - -#pragma mark - ReactionsView delegate - -- (void)didSelectReactionWithReaction:(NCChatReaction *)reaction -{ - [self.delegate cellDidSelectedReaction:reaction forMessage:self.message]; -} - -#pragma mark - Getters - -- (UILabel *)titleLabel -{ - if (!_titleLabel) { - _titleLabel = [UILabel new]; - _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; - _titleLabel.backgroundColor = [UIColor clearColor]; - _titleLabel.userInteractionEnabled = NO; - _titleLabel.numberOfLines = 1; - _titleLabel.font = [UIFont systemFontOfSize:[FileMessageTableViewCell defaultFontSize]]; - _titleLabel.textColor = [UIColor secondaryLabelColor]; - } - return _titleLabel; -} - -- (UILabel *)dateLabel -{ - if (!_dateLabel) { - _dateLabel = [UILabel new]; - _dateLabel.textAlignment = NSTextAlignmentRight; - _dateLabel.translatesAutoresizingMaskIntoConstraints = NO; - _dateLabel.backgroundColor = [UIColor clearColor]; - _dateLabel.userInteractionEnabled = NO; - _dateLabel.numberOfLines = 1; - _dateLabel.font = [UIFont systemFontOfSize:12.0]; - _dateLabel.textColor = [UIColor secondaryLabelColor]; - } - return _dateLabel; -} - -- (ReactionsView *)reactionsView -{ - if (!_reactionsView) { - UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; - flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; - _reactionsView = [[ReactionsView alloc] initWithFrame:CGRectMake(0, 0, 50, 50) collectionViewLayout:flowLayout]; - _reactionsView.translatesAutoresizingMaskIntoConstraints = NO; - _reactionsView.reactionsDelegate = self; - } - return _reactionsView; -} - -- (MessageBodyTextView *)bodyTextView -{ - if (!_bodyTextView) { - _bodyTextView = [MessageBodyTextView new]; - _bodyTextView.font = [UIFont systemFontOfSize:[FileMessageTableViewCell defaultFontSize]]; - _bodyTextView.dataDetectorTypes = UIDataDetectorTypeNone; - } - return _bodyTextView; -} - -- (void)clearFileStatusView { - if (_activityIndicator) { - [_activityIndicator stopAnimating]; - _activityIndicator = nil; - } - - [self.fileStatusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; -} - -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - - // NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; - // pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - -@end diff --git a/NextcloudTalk/FilePreviewImageView.swift b/NextcloudTalk/FilePreviewImageView.swift new file mode 100644 index 000000000..159f98d98 --- /dev/null +++ b/NextcloudTalk/FilePreviewImageView.swift @@ -0,0 +1,26 @@ +// +// Copyright (c) 2024 Marcel Müller +// +// Author Marcel Müller +// +// GNU GPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation + +public class FilePreviewImageView: UIImageView { + +} diff --git a/NextcloudTalk/GroupedChatMessageTableViewCell.h b/NextcloudTalk/GroupedChatMessageTableViewCell.h deleted file mode 100644 index 21d0b20f8..000000000 --- a/NextcloudTalk/GroupedChatMessageTableViewCell.h +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Ivan Sein - * - * @author Ivan Sein - * - * @license GNU GPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#import - -@class ReactionsView; -@class ReferenceView; -@protocol ReactionsViewDelegate; - -#import "ChatTableViewCell.h" -#import "NCChatMessage.h" -#import "MessageBodyTextView.h" - -static CGFloat kGroupedChatMessageCellMinimumHeight = 30.0; -static NSString *GroupedChatMessageCellIdentifier = @"GroupedChatMessageCellIdentifier"; - -@interface GroupedChatMessageTableViewCell : ChatTableViewCell - -@property (nonatomic, weak) id delegate; - -@property (nonatomic, strong) MessageBodyTextView *bodyTextView; -@property (nonatomic, strong) UIView *statusView; -@property (nonatomic, strong) ReactionsView *reactionsView; -@property (nonatomic, strong) NSArray *vConstraint; -@property (nonatomic, strong) ReferenceView *referenceView; - -+ (CGFloat)defaultFontSize; -- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead; - -@end diff --git a/NextcloudTalk/GroupedChatMessageTableViewCell.m b/NextcloudTalk/GroupedChatMessageTableViewCell.m deleted file mode 100644 index e72701f29..000000000 --- a/NextcloudTalk/GroupedChatMessageTableViewCell.m +++ /dev/null @@ -1,239 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Ivan Sein - * - * @author Ivan Sein - * - * @license GNU GPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#import "GroupedChatMessageTableViewCell.h" - -#import "MaterialActivityIndicator.h" -#import "SLKUIConstants.h" -#import "AFImageDownloader.h" - -#import "NCAppBranding.h" -#import "NCDatabaseManager.h" - -#import "NextcloudTalk-Swift.h" - -@implementation GroupedChatMessageTableViewCell - -- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; - if (self) { - self.backgroundColor = [NCAppBranding backgroundColor]; - [self configureSubviews]; - } - return self; -} - -- (void)configureSubviews -{ - [self.contentView addSubview:self.bodyTextView]; - - _statusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kChatCellStatusViewHeight, kChatCellStatusViewHeight)]; - _statusView.translatesAutoresizingMaskIntoConstraints = NO; - [self.contentView addSubview:_statusView]; - [self.contentView addSubview:self.reactionsView]; - [self.contentView addSubview:self.referenceView]; - - NSDictionary *views = @{@"bodyTextView": self.bodyTextView, - @"statusView": self.statusView, - @"reactionsView": self.reactionsView, - @"referenceView": self.referenceView - }; - - NSDictionary *metrics = @{@"avatar": @50, - @"statusSize": @(kChatCellStatusViewHeight), - @"padding": @15, - @"right": @10, - @"left": @5 - }; - - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-avatar-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[reactionsView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[referenceView(>=0)]-right-|" options:0 metrics:metrics views:views]]; - _vConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-left-[bodyTextView(>=0@999)]-0-[referenceView(0)]-0-[reactionsView(0)]-(>=left)-|" options:0 metrics:metrics views:views]; - [self.contentView addConstraints:_vConstraint]; - [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-left-[statusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - - CGFloat pointSize = [GroupedChatMessageTableViewCell defaultFontSize]; - - self.bodyTextView.font = [UIFont systemFontOfSize:pointSize]; - self.bodyTextView.text = @""; - - self.reactionsView.reactions = @[]; - - _vConstraint[2].constant = 0; - _vConstraint[3].constant = 0; - _vConstraint[5].constant = 0; - - [_referenceView prepareForReuse]; - - self.statusView.hidden = NO; - [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; -} - -- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead -{ - self.bodyTextView.attributedText = message.parsedMarkdownForChat; - self.messageId = message.messageId; - self.message = message; - - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - BOOL shouldShowDeliveryStatus = [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityChatReadStatus forAccountId:activeAccount.accountId]; - BOOL shouldShowReadStatus = !serverCapabilities.readStatusPrivacy; - - if (message.isDeleting) { - [self setDeliveryState:ChatMessageDeliveryStateDeleting]; - } else if (message.sendingFailed) { - [self setDeliveryState:ChatMessageDeliveryStateFailed]; - } else if (message.isTemporary){ - [self setDeliveryState:ChatMessageDeliveryStateSending]; - } else if ([message.actorId isEqualToString:activeAccount.userId] && [message.actorType isEqualToString:@"users"] && shouldShowDeliveryStatus) { - if (lastCommonRead >= message.messageId && shouldShowReadStatus) { - [self setDeliveryState:ChatMessageDeliveryStateRead]; - } else { - [self setDeliveryState:ChatMessageDeliveryStateSent]; - } - } - - if (message.isDeletedMessage) { - self.statusView.hidden = YES; - self.bodyTextView.textColor = [UIColor tertiaryLabelColor]; - } - [self.reactionsView updateReactionsWithReactions:message.reactionsArray]; - if (message.reactionsArray.count > 0) { - _vConstraint[5].constant = 40; - } - - if (message.containsURL) { - _vConstraint[2].constant = 5; - _vConstraint[3].constant = 100; - - [message getReferenceDataWithCompletionBlock:^(NCChatMessage *message, NSDictionary *referenceData, NSString *url) { - if (![self.message isSameMessage:message]) { - return; - } - - if (!referenceData && message.deckCard) { - // In case we were unable to retrieve reference data (for example if the user has no permissions) - // but the message is a shared deck card, we use the shared information to show the deck view - [self.referenceView updateFor:message.deckCard]; - } else { - [self.referenceView updateFor:referenceData and:url]; - } - }]; - } - - if (self.message.isReplyable && !self.message.isDeleting) { - __weak typeof(self) weakSelf = self; - [self addReplyGestureWithActionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { - __strong typeof(self) strongSelf = weakSelf; - [strongSelf.delegate cellWantsToReplyToMessage:strongSelf.message]; - }]; - } -} - -- (void)setDeliveryState:(ChatMessageDeliveryState)state -{ - [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; - - if (state == ChatMessageDeliveryStateSending || state == ChatMessageDeliveryStateDeleting) { - MDCActivityIndicator *activityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - activityIndicator.radius = 7.0f; - activityIndicator.cycleColors = @[UIColor.lightGrayColor]; - [activityIndicator startAnimating]; - [self.statusView addSubview:activityIndicator]; - } else if (state == ChatMessageDeliveryStateFailed) { - UIImageView *errorView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [errorView setImage:[UIImage imageNamed:@"error"]]; - [self.statusView addSubview:errorView]; - } else if (state == ChatMessageDeliveryStateSent) { - UIImageView *checkView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [checkView setImage:[UIImage imageNamed:@"check"]]; - checkView.image = [checkView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - [checkView setTintColor:[UIColor lightGrayColor]]; - [self.statusView addSubview:checkView]; - } else if (state == ChatMessageDeliveryStateRead) { - UIImageView *checkAllView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - [checkAllView setImage:[UIImage imageNamed:@"check-all"]]; - checkAllView.image = [checkAllView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - [checkAllView setTintColor:[UIColor lightGrayColor]]; - [self.statusView addSubview:checkAllView]; - } -} - -#pragma mark - ReactionsView delegate - -- (void)didSelectReactionWithReaction:(NCChatReaction *)reaction -{ - [self.delegate cellDidSelectedReaction:reaction forMessage:self.message]; -} - -#pragma mark - Getters - -- (MessageBodyTextView *)bodyTextView -{ - if (!_bodyTextView) { - _bodyTextView = [MessageBodyTextView new]; - _bodyTextView.font = [UIFont systemFontOfSize:[GroupedChatMessageTableViewCell defaultFontSize]]; - } - return _bodyTextView; -} - -- (ReactionsView *)reactionsView -{ - if (!_reactionsView) { - UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; - flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; - _reactionsView = [[ReactionsView alloc] initWithFrame:CGRectMake(0, 0, 50, 50) collectionViewLayout:flowLayout]; - _reactionsView.translatesAutoresizingMaskIntoConstraints = NO; - _reactionsView.reactionsDelegate = self; - } - return _reactionsView; -} - -- (ReferenceView *)referenceView -{ - if (!_referenceView) { - _referenceView = [[ReferenceView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; - _referenceView.translatesAutoresizingMaskIntoConstraints = NO; - } - return _referenceView; -} - -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - -// NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; -// pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - -@end diff --git a/NextcloudTalk/NCMessageFileParameter.h b/NextcloudTalk/NCMessageFileParameter.h index e565f3b96..382bdc348 100644 --- a/NextcloudTalk/NCMessageFileParameter.h +++ b/NextcloudTalk/NCMessageFileParameter.h @@ -30,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN @interface NCMessageFileParameter : NCMessageParameter -@property (nonatomic, strong) NSString *path; +@property (nonatomic, strong) NSString * _Nullable path; @property (nonatomic, strong) NSString *mimetype; @property (nonatomic, assign) NSInteger size; @property (nonatomic, assign) BOOL previewAvailable; diff --git a/NextcloudTalk/NCMessageParameter.h b/NextcloudTalk/NCMessageParameter.h index f590367ec..c306aeaa5 100644 --- a/NextcloudTalk/NCMessageParameter.h +++ b/NextcloudTalk/NCMessageParameter.h @@ -27,7 +27,7 @@ @property (nonatomic, strong) NSString *parameterId; @property (nonatomic, strong) NSString *name; -@property (nonatomic, strong) NSString *link; +@property (nonatomic, strong) NSString * _Nullable link; @property (nonatomic, strong) NSString *type; @property (nonatomic, assign) NSRange range; @property (nonatomic, strong) NSString *contactName; @@ -38,7 +38,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)parameterDict; - (BOOL)shouldBeHighlighted; -- (UIImage *)contactPhotoImage; +- (UIImage * _Nullable)contactPhotoImage; // parametersDict as [NSString:NCMessageParameter] + (NSString *)messageParametersJSONStringFromDictionary:(NSDictionary *)parametersDict; diff --git a/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h b/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h index 171b15b86..be2e0c116 100644 --- a/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h +++ b/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h @@ -34,7 +34,6 @@ #import "NCMessageTextView.h" #import "ReplyMessageView.h" #import "NCSettingsController.h" -#import "ChatMessageTableViewCell.h" #import "AutoCompletionTableViewCell.h" #import "NCKeyChainController.h" #import "CCCertificate.h" diff --git a/NextcloudTalk/NextcloudTalk-Bridging-Header.h b/NextcloudTalk/NextcloudTalk-Bridging-Header.h index f8be03221..012ee6cac 100644 --- a/NextcloudTalk/NextcloudTalk-Bridging-Header.h +++ b/NextcloudTalk/NextcloudTalk-Bridging-Header.h @@ -70,9 +70,6 @@ #import "VoiceMessageRecordingView.h" #import "CallKitManager.h" -#import "ChatMessageTableViewCell.h" -#import "GroupedChatMessageTableViewCell.h" -#import "FileMessageTableViewCell.h" #import "GeoLocationRichObject.h" #import "LocationMessageTableViewCell.h" #import "MessageSeparatorTableViewCell.h" diff --git a/NextcloudTalk/UIFontExtension.swift b/NextcloudTalk/UIFontExtension.swift new file mode 100644 index 000000000..24f4bc81c --- /dev/null +++ b/NextcloudTalk/UIFontExtension.swift @@ -0,0 +1,61 @@ +// +// Copyright (c) 2024 Marcel Müller +// +// Author Marcel Müller +// +// GNU GPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import UIKit + +extension UIFont { + + static func monospacedPreferredFont(forTextStyle style: TextStyle) -> UIFont { + let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) + let font = UIFont.monospacedSystemFont(ofSize: fontDescriptor.pointSize, weight: .regular) + + return UIFontMetrics(forTextStyle: style).scaledFont(for: font) + } + + // See: https://stackoverflow.com/a/62687023 + static func preferredFont(for style: TextStyle, weight: Weight, italic: Bool) -> UIFont { + // Get the style's default pointSize + let traits = UITraitCollection(preferredContentSizeCategory: .large) + let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traits) + + // Get the font at the default size and preferred weight + var font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight) + if italic == true { + font = font.with([.traitItalic]) + } + + // Setup the font to be auto-scalable + let metrics = UIFontMetrics(forTextStyle: style) + return metrics.scaledFont(for: font) + } + + @objc + static func preferredFont(for style: TextStyle, weight: Weight) -> UIFont { + return preferredFont(for: style, weight: weight, italic: false) + } + + private func with(_ traits: UIFontDescriptor.SymbolicTraits...) -> UIFont { + guard let descriptor = fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits).union(fontDescriptor.symbolicTraits)) else { + return self + } + return UIFont(descriptor: descriptor, size: 0) + } +} diff --git a/NextcloudTalkTests/Unit/UnitChatCellTest.swift b/NextcloudTalkTests/Unit/UnitChatCellTest.swift index 504e3fdfa..295133e1b 100644 --- a/NextcloudTalkTests/Unit/UnitChatCellTest.swift +++ b/NextcloudTalkTests/Unit/UnitChatCellTest.swift @@ -82,7 +82,7 @@ final class UnitChatCellTest: TestBaseRealm { // Multiline chat message testMessage.message = "test\nasd\nasd" - XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 108.0) + XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 110.0) // Normal chat message with reaction testMessage.message = "test" @@ -197,7 +197,7 @@ final class UnitChatCellTest: TestBaseRealm { testMessage.messageParametersJSONString = fileMessageParameters testMessage.message = "File caption... https://nextcloud.com" - XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 210.0) + XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 315.0) } func testCellWithFileAndQuoteHeight() { @@ -215,10 +215,7 @@ final class UnitChatCellTest: TestBaseRealm { } testMessage.parentId = "internal-1" - XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 210.0) - - // This should be 275 if the file cell would be able to display a quoted view - // XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 275.0) + XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 275.0) } func testCellWithVoiceMessageHeight() { From d7c907bb20359581658acab9fb4ea6c9651d6232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sun, 17 Mar 2024 23:06:13 +0100 Subject: [PATCH 05/11] Move from fixed font size to dynamic font size in legacy cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/AutoCompletionTableViewCell.h | 1 - NextcloudTalk/AutoCompletionTableViewCell.m | 18 +++---------- NextcloudTalk/BaseChatViewController.swift | 2 +- NextcloudTalk/LocationMessageTableViewCell.h | 1 - NextcloudTalk/LocationMessageTableViewCell.m | 26 +++++-------------- NextcloudTalk/MessageBodyTextView.m | 2 ++ NextcloudTalk/NCChatMessage.m | 7 +++-- .../ObjectShareMessageTableViewCell.h | 1 - .../ObjectShareMessageTableViewCell.m | 24 +++++------------ NextcloudTalk/QuotedMessageView.m | 4 +-- NextcloudTalk/ReferenceDefaultView.xib | 10 +++---- NextcloudTalk/RoomTableViewCell.m | 12 ++++----- NextcloudTalk/RoomTableViewCell.xib | 16 ++++++------ NextcloudTalk/SwiftMarkdownObjCBridge.swift | 8 +++--- NextcloudTalk/SystemMessageTableViewCell.h | 1 - NextcloudTalk/SystemMessageTableViewCell.m | 16 ++---------- NextcloudTalk/VoiceMessageTableViewCell.h | 1 - NextcloudTalk/VoiceMessageTableViewCell.m | 26 +++++-------------- .../ShareConfirmationViewController.swift | 2 +- 19 files changed, 57 insertions(+), 121 deletions(-) diff --git a/NextcloudTalk/AutoCompletionTableViewCell.h b/NextcloudTalk/AutoCompletionTableViewCell.h index f75a3d52f..9611ff186 100644 --- a/NextcloudTalk/AutoCompletionTableViewCell.h +++ b/NextcloudTalk/AutoCompletionTableViewCell.h @@ -33,7 +33,6 @@ static NSString *AutoCompletionCellIdentifier = @"AutoCompletionCellIdentifier @property (nonatomic, strong) AvatarButton *avatarButton; @property (nonatomic, strong) UIImageView *userStatusImageView; -+ (CGFloat)defaultFontSize; - (void)setUserStatus:(NSString *)userStatus; @end diff --git a/NextcloudTalk/AutoCompletionTableViewCell.m b/NextcloudTalk/AutoCompletionTableViewCell.m index 90d7aa763..7efed73c1 100644 --- a/NextcloudTalk/AutoCompletionTableViewCell.m +++ b/NextcloudTalk/AutoCompletionTableViewCell.m @@ -21,6 +21,7 @@ */ #import "AutoCompletionTableViewCell.h" +#import "ChatTableViewCell.h" #import "SLKUIConstants.h" @@ -85,10 +86,8 @@ - (void)configureSubviews - (void)prepareForReuse { [super prepareForReuse]; - - CGFloat pointSize = [AutoCompletionTableViewCell defaultFontSize]; - self.titleLabel.font = [UIFont systemFontOfSize:pointSize]; + self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; self.titleLabel.text = @""; [self.avatarButton cancelCurrentRequest]; @@ -108,7 +107,7 @@ - (UILabel *)titleLabel _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.userInteractionEnabled = NO; _titleLabel.numberOfLines = 1; - _titleLabel.font = [UIFont systemFontOfSize:[AutoCompletionTableViewCell defaultFontSize]]; + _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _titleLabel.textColor = [UIColor secondaryLabelColor]; } return _titleLabel; @@ -137,15 +136,4 @@ - (void)setUserStatus:(NSString *)userStatus } } -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - -// NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; -// pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - - @end diff --git a/NextcloudTalk/BaseChatViewController.swift b/NextcloudTalk/BaseChatViewController.swift index 75f3f80ae..68d65da37 100644 --- a/NextcloudTalk/BaseChatViewController.swift +++ b/NextcloudTalk/BaseChatViewController.swift @@ -2691,7 +2691,7 @@ import QuickLook width -= message.isSystemMessage() ? 80.0 : 30.0 // *right(10) + dateLabel(40) : 3*right(10) if message.poll() != nil { - messageString = messageString.withFont(.systemFont(ofSize: ObjectShareMessageTableViewCell.defaultFontSize())) + messageString = messageString.withFont(.preferredFont(forTextStyle: .body)) width -= kObjectShareMessageCellObjectTypeImageSize + 25 // 2*right(10) + left(5) } diff --git a/NextcloudTalk/LocationMessageTableViewCell.h b/NextcloudTalk/LocationMessageTableViewCell.h index 69f307ab9..29fa6dfa8 100644 --- a/NextcloudTalk/LocationMessageTableViewCell.h +++ b/NextcloudTalk/LocationMessageTableViewCell.h @@ -60,7 +60,6 @@ static NSString *GroupedLocationMessageCellIdentifier = @"GroupedLocationMessa @property (nonatomic, strong) NSArray *vConstraints; @property (nonatomic, strong) NSArray *vGroupedConstraints; -+ (CGFloat)defaultFontSize; - (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead; @end diff --git a/NextcloudTalk/LocationMessageTableViewCell.m b/NextcloudTalk/LocationMessageTableViewCell.m index 962caec0a..f16cedeca 100644 --- a/NextcloudTalk/LocationMessageTableViewCell.m +++ b/NextcloudTalk/LocationMessageTableViewCell.m @@ -131,12 +131,10 @@ - (void)configureSubviews - (void)prepareForReuse { [super prepareForReuse]; - - CGFloat pointSize = [LocationMessageTableViewCell defaultFontSize]; - - self.titleLabel.font = [UIFont systemFontOfSize:pointSize]; - self.bodyTextView.font = [UIFont systemFontOfSize:pointSize]; - + + self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; + self.bodyTextView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; + self.titleLabel.text = @""; self.bodyTextView.text = @""; self.dateLabel.text = @""; @@ -292,7 +290,7 @@ - (UILabel *)titleLabel _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.userInteractionEnabled = NO; _titleLabel.numberOfLines = 1; - _titleLabel.font = [UIFont systemFontOfSize:[LocationMessageTableViewCell defaultFontSize]]; + _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _titleLabel.textColor = [UIColor secondaryLabelColor]; } return _titleLabel; @@ -307,7 +305,7 @@ - (UILabel *)dateLabel _dateLabel.backgroundColor = [UIColor clearColor]; _dateLabel.userInteractionEnabled = NO; _dateLabel.numberOfLines = 1; - _dateLabel.font = [UIFont systemFontOfSize:12.0]; + _dateLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]; _dateLabel.textColor = [UIColor secondaryLabelColor]; } return _dateLabel; @@ -329,20 +327,10 @@ - (MessageBodyTextView *)bodyTextView { if (!_bodyTextView) { _bodyTextView = [MessageBodyTextView new]; - _bodyTextView.font = [UIFont systemFontOfSize:[LocationMessageTableViewCell defaultFontSize]]; + _bodyTextView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _bodyTextView.dataDetectorTypes = UIDataDetectorTypeNone; } return _bodyTextView; } -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - - // NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; - // pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - @end diff --git a/NextcloudTalk/MessageBodyTextView.m b/NextcloudTalk/MessageBodyTextView.m index 2c4641b9d..aed412142 100644 --- a/NextcloudTalk/MessageBodyTextView.m +++ b/NextcloudTalk/MessageBodyTextView.m @@ -49,6 +49,8 @@ - (instancetype)init self.textContainer.lineFragmentPadding = 0; self.textContainerInset = UIEdgeInsetsZero; self.translatesAutoresizingMaskIntoConstraints = NO; + + // Set background color to clear to allow cell selection color to be visible self.backgroundColor = [UIColor clearColor]; self.editable = NO; self.scrollEnabled = NO; diff --git a/NextcloudTalk/NCChatMessage.m b/NextcloudTalk/NCChatMessage.m index a296bf5b2..7b552717d 100644 --- a/NextcloudTalk/NCChatMessage.m +++ b/NextcloudTalk/NCChatMessage.m @@ -513,7 +513,7 @@ - (NSMutableAttributedString *)parsedMessage if (self.isEmojiMessage) { [attributedMessage addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:36.0f] range:NSMakeRange(0, parsedMessage.length)]; } else { - [attributedMessage addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:16.0f] range:NSMakeRange(0, parsedMessage.length)]; + [attributedMessage addAttribute:NSFontAttributeName value:[UIFont preferredFontForTextStyle:UIFontTextStyleBody] range:NSMakeRange(0, parsedMessage.length)]; } UIColor *highlightedColor = nil; @@ -534,13 +534,13 @@ - (NSMutableAttributedString *)parsedMessage [attributedMessage addAttribute:NSForegroundColorAttributeName value:defaultColor range:param.range]; } - [attributedMessage addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:16.0f] range:param.range]; + [attributedMessage addAttribute:NSFontAttributeName value:[UIFont preferredFontFor:UIFontTextStyleBody weight:UIFontWeightBold] range:param.range]; } //Create a link if parameter contains a link else if (param.link) { // Do not create links for files. File preview images will redirect to files client or browser. if ([param.type isEqualToString:@"file"]) { - [attributedMessage addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:16.0f] range:param.range]; + [attributedMessage addAttribute:NSFontAttributeName value:[UIFont preferredFontFor:UIFontTextStyleBody weight:UIFontWeightBold] range:param.range]; } else { [attributedMessage addAttribute:NSLinkAttributeName value:param.link range:param.range]; } @@ -589,7 +589,6 @@ - (NSMutableAttributedString *)systemMessageFormat { NSMutableAttributedString *message = [self parsedMessage]; - //TODO: Further adjust for dark-mode ? [message addAttribute:NSForegroundColorAttributeName value:[UIColor tertiaryLabelColor] range:NSMakeRange(0,message.length)]; return message; diff --git a/NextcloudTalk/ObjectShareMessageTableViewCell.h b/NextcloudTalk/ObjectShareMessageTableViewCell.h index 1dc2a2c94..c127f5aff 100644 --- a/NextcloudTalk/ObjectShareMessageTableViewCell.h +++ b/NextcloudTalk/ObjectShareMessageTableViewCell.h @@ -57,7 +57,6 @@ static NSString *GroupedObjectShareMessageCellIdentifier = @"GroupedObjectSha @property (nonatomic, strong) NSArray *vConstraints; @property (nonatomic, strong) NSArray *vGroupedConstraints; -+ (CGFloat)defaultFontSize; - (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead; @end diff --git a/NextcloudTalk/ObjectShareMessageTableViewCell.m b/NextcloudTalk/ObjectShareMessageTableViewCell.m index 3c43d4912..6adfcb5d7 100644 --- a/NextcloudTalk/ObjectShareMessageTableViewCell.m +++ b/NextcloudTalk/ObjectShareMessageTableViewCell.m @@ -76,7 +76,7 @@ - (void)configureSubviews _objectTitle.editable = NO; _objectTitle.scrollEnabled = NO; _objectTitle.userInteractionEnabled = NO; - _objectTitle.font = [UIFont systemFontOfSize:[ObjectShareMessageTableViewCell defaultFontSize]]; + _objectTitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; [_objectContainerView addSubview:_objectTitle]; UITapGestureRecognizer *previewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(objectTapped:)]; @@ -142,11 +142,9 @@ - (void)configureSubviews - (void)prepareForReuse { [super prepareForReuse]; - - CGFloat pointSize = [ObjectShareMessageTableViewCell defaultFontSize]; - - self.titleLabel.font = [UIFont systemFontOfSize:pointSize]; - + + self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; + self.titleLabel.text = @""; self.dateLabel.text = @""; @@ -273,7 +271,7 @@ - (UILabel *)titleLabel _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.userInteractionEnabled = NO; _titleLabel.numberOfLines = 1; - _titleLabel.font = [UIFont systemFontOfSize:[ObjectShareMessageTableViewCell defaultFontSize]]; + _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _titleLabel.textColor = [UIColor secondaryLabelColor]; } return _titleLabel; @@ -288,7 +286,7 @@ - (UILabel *)dateLabel _dateLabel.backgroundColor = [UIColor clearColor]; _dateLabel.userInteractionEnabled = NO; _dateLabel.numberOfLines = 1; - _dateLabel.font = [UIFont systemFontOfSize:12.0]; + _dateLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]; _dateLabel.textColor = [UIColor secondaryLabelColor]; } return _dateLabel; @@ -306,14 +304,4 @@ - (ReactionsView *)reactionsView return _reactionsView; } -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - - // NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; - // pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - @end diff --git a/NextcloudTalk/QuotedMessageView.m b/NextcloudTalk/QuotedMessageView.m index dabc255ce..3b476f48b 100644 --- a/NextcloudTalk/QuotedMessageView.m +++ b/NextcloudTalk/QuotedMessageView.m @@ -96,7 +96,7 @@ - (UILabel *)actorLabel _actorLabel.numberOfLines = 1; _actorLabel.contentMode = UIViewContentModeLeft; - _actorLabel.font = [UIFont systemFontOfSize:16.0]; + _actorLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _actorLabel.textColor = [UIColor secondaryLabelColor]; } return _actorLabel; @@ -112,7 +112,7 @@ - (UILabel *)messageLabel _messageLabel.numberOfLines = 0; _messageLabel.contentMode = UIViewContentModeLeft; - _messageLabel.font = [UIFont systemFontOfSize:16.0]; + _messageLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _messageLabel.textColor = [NCAppBranding chatForegroundColor]; } return _messageLabel; diff --git a/NextcloudTalk/ReferenceDefaultView.xib b/NextcloudTalk/ReferenceDefaultView.xib index bd6f36e25..602dc66cf 100644 --- a/NextcloudTalk/ReferenceDefaultView.xib +++ b/NextcloudTalk/ReferenceDefaultView.xib @@ -1,9 +1,9 @@ - + - + @@ -35,7 +35,7 @@ - + @@ -47,7 +47,7 @@ Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - + diff --git a/NextcloudTalk/RoomTableViewCell.m b/NextcloudTalk/RoomTableViewCell.m index 36c1ab407..7fe15c6f9 100644 --- a/NextcloudTalk/RoomTableViewCell.m +++ b/NextcloudTalk/RoomTableViewCell.m @@ -132,14 +132,14 @@ - (void)setUnreadMessages:(NSInteger)number mentioned:(BOOL)mentioned groupMenti _unreadMessages = number; if (number > 0) { - _titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightBold]; - _subtitleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightBold]; - _dateLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold]; + _titleLabel.font = [UIFont preferredFontFor:UIFontTextStyleHeadline weight:UIFontWeightBold]; + _subtitleLabel.font = [UIFont preferredFontFor:UIFontTextStyleCallout weight:UIFontWeightBold]; + _dateLabel.font = [UIFont preferredFontFor:UIFontTextStyleFootnote weight:UIFontWeightSemibold]; _unreadMessagesView.hidden = NO; } else { - _titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; - _subtitleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightRegular]; - _dateLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightRegular]; + _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; + _subtitleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCallout]; + _dateLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]; _unreadMessagesView.hidden = YES; } diff --git a/NextcloudTalk/RoomTableViewCell.xib b/NextcloudTalk/RoomTableViewCell.xib index 832945b3d..7ed1587e4 100644 --- a/NextcloudTalk/RoomTableViewCell.xib +++ b/NextcloudTalk/RoomTableViewCell.xib @@ -27,22 +27,22 @@ - diff --git a/NextcloudTalk/SwiftMarkdownObjCBridge.swift b/NextcloudTalk/SwiftMarkdownObjCBridge.swift index 62dc90467..7caef3cfa 100644 --- a/NextcloudTalk/SwiftMarkdownObjCBridge.swift +++ b/NextcloudTalk/SwiftMarkdownObjCBridge.swift @@ -26,13 +26,13 @@ import UIKit @objcMembers class SwiftMarkdownObjCBridge: NSObject { static let markdownParser: CDMarkdownParser = { - let markdownParser = CDMarkdownParser(font: .systemFont(ofSize: 16), fontColor: NCAppBranding.chatForegroundColor()) + let markdownParser = CDMarkdownParser(font: .preferredFont(forTextStyle: .body), fontColor: NCAppBranding.chatForegroundColor()) markdownParser.code.backgroundColor = .secondarySystemBackground - markdownParser.code.font = CDFont.monospacedSystemFont(ofSize: 16, weight: .regular) + markdownParser.code.font = .monospacedPreferredFont(forTextStyle: .body) markdownParser.syntax.backgroundColor = .secondarySystemBackground - markdownParser.syntax.font = CDFont.monospacedSystemFont(ofSize: 16, weight: .regular) + markdownParser.syntax.font = .monospacedPreferredFont(forTextStyle: .body) markdownParser.squashNewlines = false markdownParser.overwriteExistingStyle = false @@ -46,7 +46,7 @@ import UIKit markdownParser.list.color = nil // To correctly position list elements, we need to tell CDMarkdownKit the font to use for sizing - markdownParser.list.indicatorFont = .systemFont(ofSize: 16) + markdownParser.list.indicatorFont = .preferredFont(forTextStyle: .body) markdownParser.quote.font = nil markdownParser.quote.color = nil diff --git a/NextcloudTalk/SystemMessageTableViewCell.h b/NextcloudTalk/SystemMessageTableViewCell.h index 5a461914f..8302d7f32 100644 --- a/NextcloudTalk/SystemMessageTableViewCell.h +++ b/NextcloudTalk/SystemMessageTableViewCell.h @@ -45,7 +45,6 @@ static NSString *InvisibleSystemMessageCellIdentifier = @"InvisibleSystemMessa @property (nonatomic, strong) MessageBodyTextView *bodyTextView; @property (nonatomic, strong) UIButton *collapseButton; -+ (CGFloat)defaultFontSize; - (void)setupForMessage:(NCChatMessage *)message; @end diff --git a/NextcloudTalk/SystemMessageTableViewCell.m b/NextcloudTalk/SystemMessageTableViewCell.m index 63f99cb8e..4cf002d79 100644 --- a/NextcloudTalk/SystemMessageTableViewCell.m +++ b/NextcloudTalk/SystemMessageTableViewCell.m @@ -80,8 +80,7 @@ - (void)prepareForReuse self.selectionStyle = UITableViewCellSelectionStyleNone; self.backgroundColor = [NCAppBranding backgroundColor]; - CGFloat pointSize = [SystemMessageTableViewCell defaultFontSize]; - self.bodyTextView.font = [UIFont systemFontOfSize:pointSize]; + self.bodyTextView.text = @""; self.dateLabel.text = @""; } @@ -92,7 +91,6 @@ - (MessageBodyTextView *)bodyTextView { if (!_bodyTextView) { _bodyTextView = [MessageBodyTextView new]; - _bodyTextView.font = [UIFont systemFontOfSize:[SystemMessageTableViewCell defaultFontSize]]; } return _bodyTextView; } @@ -106,7 +104,7 @@ - (UILabel *)dateLabel _dateLabel.backgroundColor = [UIColor clearColor]; _dateLabel.userInteractionEnabled = NO; _dateLabel.numberOfLines = 1; - _dateLabel.font = [UIFont systemFontOfSize:12.0]; + _dateLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]; _dateLabel.textColor = [UIColor secondaryLabelColor]; } return _dateLabel; @@ -129,16 +127,6 @@ - (void)collapseButtonPressed [self.delegate cellWantsToCollapseMessagesWithMessage:self.message]; } -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - - // NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; - // pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - - (void)setupForMessage:(NCChatMessage *)message { self.collapseButton.hidden = (message.isCollapsed || message.collapsedMessages.count == 0); diff --git a/NextcloudTalk/VoiceMessageTableViewCell.h b/NextcloudTalk/VoiceMessageTableViewCell.h index 112a43221..fb63c3d6e 100644 --- a/NextcloudTalk/VoiceMessageTableViewCell.h +++ b/NextcloudTalk/VoiceMessageTableViewCell.h @@ -62,7 +62,6 @@ static NSString *GroupedVoiceMessageCellIdentifier = @"GroupedVoiceMessageCell @property (nonatomic, strong) NSArray *vConstraints; @property (nonatomic, strong) NSArray *vGroupedConstraints; -+ (CGFloat)defaultFontSize; - (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead; - (void)setPlayerProgress:(CGFloat)progress isPlaying:(BOOL)playing maximumValue:(CGFloat)maxValue; - (void)resetPlayer; diff --git a/NextcloudTalk/VoiceMessageTableViewCell.m b/NextcloudTalk/VoiceMessageTableViewCell.m index d6d78c44f..b49b2086d 100644 --- a/NextcloudTalk/VoiceMessageTableViewCell.m +++ b/NextcloudTalk/VoiceMessageTableViewCell.m @@ -166,12 +166,10 @@ - (void)configureSubviews - (void)prepareForReuse { [super prepareForReuse]; - - CGFloat pointSize = [VoiceMessageTableViewCell defaultFontSize]; - - self.titleLabel.font = [UIFont systemFontOfSize:pointSize]; - self.bodyTextView.font = [UIFont systemFontOfSize:pointSize]; - + + self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; + self.bodyTextView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; + self.titleLabel.text = @""; self.bodyTextView.text = @""; self.dateLabel.text = @""; @@ -399,7 +397,7 @@ - (UILabel *)titleLabel _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.userInteractionEnabled = NO; _titleLabel.numberOfLines = 1; - _titleLabel.font = [UIFont systemFontOfSize:[VoiceMessageTableViewCell defaultFontSize]]; + _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _titleLabel.textColor = [UIColor secondaryLabelColor]; } return _titleLabel; @@ -414,7 +412,7 @@ - (UILabel *)dateLabel _dateLabel.backgroundColor = [UIColor clearColor]; _dateLabel.userInteractionEnabled = NO; _dateLabel.numberOfLines = 1; - _dateLabel.font = [UIFont systemFontOfSize:12.0]; + _dateLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]; _dateLabel.textColor = [UIColor secondaryLabelColor]; } return _dateLabel; @@ -436,7 +434,7 @@ - (MessageBodyTextView *)bodyTextView { if (!_bodyTextView) { _bodyTextView = [MessageBodyTextView new]; - _bodyTextView.font = [UIFont systemFontOfSize:[VoiceMessageTableViewCell defaultFontSize]]; + _bodyTextView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; _bodyTextView.dataDetectorTypes = UIDataDetectorTypeNone; } return _bodyTextView; @@ -467,14 +465,4 @@ - (void)clearFileStatusView { [self.fileStatusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; } -+ (CGFloat)defaultFontSize -{ - CGFloat pointSize = 16.0; - - // NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; - // pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); - - return pointSize; -} - @end diff --git a/ShareExtension/ShareConfirmationViewController.swift b/ShareExtension/ShareConfirmationViewController.swift index caf17120b..de6f3c589 100644 --- a/ShareExtension/ShareConfirmationViewController.swift +++ b/ShareExtension/ShareConfirmationViewController.swift @@ -213,7 +213,7 @@ import AVFoundation private lazy var shareTextView: UITextView = { let textView = UITextView() - textView.font = .systemFont(ofSize: 16) + textView.font = .preferredFont(forTextStyle: .body) textView.translatesAutoresizingMaskIntoConstraints = false textView.isHidden = true return textView From 7f58c9f49dc1585516337971f844d5dae9744937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Mon, 18 Mar 2024 00:06:41 +0100 Subject: [PATCH 06/11] Fixed font size to fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- .github/workflows/uitests.yml | 2 +- .../xcshareddata/xcschemes/NextcloudTalk.xcscheme | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/uitests.yml b/.github/workflows/uitests.yml index 3808cd1e6..eb64390f1 100644 --- a/.github/workflows/uitests.yml +++ b/.github/workflows/uitests.yml @@ -111,7 +111,7 @@ jobs: -test-iterations 3 \ -retry-tests-on-failure \ -resultBundlePath "testResult.xcresult" \ - | xcpretty + -quiet - name: Upload test results uses: actions/upload-artifact@v4 diff --git a/NextcloudTalk.xcodeproj/xcshareddata/xcschemes/NextcloudTalk.xcscheme b/NextcloudTalk.xcodeproj/xcshareddata/xcschemes/NextcloudTalk.xcscheme index 8af970d8a..6aec03222 100644 --- a/NextcloudTalk.xcodeproj/xcshareddata/xcschemes/NextcloudTalk.xcscheme +++ b/NextcloudTalk.xcodeproj/xcshareddata/xcschemes/NextcloudTalk.xcscheme @@ -26,7 +26,17 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO"> + + + + + + From e75b165d70d949ab410c1532e1064cd309dfe1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Mon, 18 Mar 2024 12:16:05 +0100 Subject: [PATCH 07/11] Use xcbeautify instead of xcpretty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- .github/workflows/uitests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/uitests.yml b/.github/workflows/uitests.yml index eb64390f1..62ba120d3 100644 --- a/.github/workflows/uitests.yml +++ b/.github/workflows/uitests.yml @@ -111,7 +111,7 @@ jobs: -test-iterations 3 \ -retry-tests-on-failure \ -resultBundlePath "testResult.xcresult" \ - -quiet + | xcbeautify --quieter - name: Upload test results uses: actions/upload-artifact@v4 From f92f4f49ffe03d18de8512e56e383b0dd6e1bcfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Mon, 18 Mar 2024 16:53:22 +0100 Subject: [PATCH 08/11] Add height constraints to status view elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- .../BaseChatTableViewCell+File.swift | 1 + NextcloudTalk/BaseChatTableViewCell.swift | 31 ++++++++++++------- NextcloudTalk/BaseChatTableViewCell.xib | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/NextcloudTalk/BaseChatTableViewCell+File.swift b/NextcloudTalk/BaseChatTableViewCell+File.swift index 907f4c536..74ad8b7e0 100644 --- a/NextcloudTalk/BaseChatTableViewCell+File.swift +++ b/NextcloudTalk/BaseChatTableViewCell+File.swift @@ -287,6 +287,7 @@ extension BaseChatTableViewCell { } fileActivityIndicator.startAnimating() + fileActivityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true self.statusView.addArrangedSubview(fileActivityIndicator) } diff --git a/NextcloudTalk/BaseChatTableViewCell.swift b/NextcloudTalk/BaseChatTableViewCell.swift index a3cae5253..0f477891c 100644 --- a/NextcloudTalk/BaseChatTableViewCell.swift +++ b/NextcloudTalk/BaseChatTableViewCell.swift @@ -298,31 +298,40 @@ class BaseChatTableViewCell: UITableViewCell, ReactionsViewDelegate { if deliveryState == ChatMessageDeliveryStateSending || deliveryState == ChatMessageDeliveryStateDeleting { let activityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20)) + activityIndicator.radius = 7.0 - activityIndicator.cycleColors = [.lightGray] + activityIndicator.cycleColors = [.systemGray2] activityIndicator.startAnimating() + activityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true + self.statusView.addArrangedSubview(activityIndicator) + } else if deliveryState == ChatMessageDeliveryStateFailed { let errorView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) let errorImage = UIImage(systemName: "exclamationmark.circle")?.withTintColor(.red).withRenderingMode(.alwaysOriginal) + errorView.image = errorImage + errorView.heightAnchor.constraint(equalToConstant: 20).isActive = true + self.statusView.addArrangedSubview(errorView) - } else if deliveryState == ChatMessageDeliveryStateSent { + + } else if deliveryState == ChatMessageDeliveryStateSent || deliveryState == ChatMessageDeliveryStateRead { + var checkImageName = "check" + + if deliveryState == ChatMessageDeliveryStateRead { + checkImageName = "check-all" + } + + let checkImage = UIImage(named: checkImageName)?.withRenderingMode(.alwaysTemplate) let checkView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) - let checkImage = UIImage(named: "check")?.withRenderingMode(.alwaysTemplate) + checkView.image = checkImage checkView.contentMode = .scaleAspectFit checkView.tintColor = .systemGray2 checkView.accessibilityIdentifier = "MessageSent" + checkView.heightAnchor.constraint(equalToConstant: 20).isActive = true + self.statusView.addArrangedSubview(checkView) - } else if deliveryState == ChatMessageDeliveryStateRead { - let checkAllView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) - let checkAllImage = UIImage(named: "check-all")?.withRenderingMode(.alwaysTemplate) - checkAllView.image = checkAllImage - checkAllView.contentMode = .scaleAspectFit - checkAllView.tintColor = .systemGray2 - checkAllView.accessibilityIdentifier = "MessageSent" - self.statusView.addArrangedSubview(checkAllView) } } diff --git a/NextcloudTalk/BaseChatTableViewCell.xib b/NextcloudTalk/BaseChatTableViewCell.xib index 87d889834..41f41f612 100644 --- a/NextcloudTalk/BaseChatTableViewCell.xib +++ b/NextcloudTalk/BaseChatTableViewCell.xib @@ -70,7 +70,7 @@ - + From fcdedcfb55935986ad99271ff60a81109c7821bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Tue, 19 Mar 2024 18:37:47 +0100 Subject: [PATCH 09/11] Progress is a CGFloat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/BaseChatTableViewCell+File.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NextcloudTalk/BaseChatTableViewCell+File.swift b/NextcloudTalk/BaseChatTableViewCell+File.swift index 74ad8b7e0..c2328876c 100644 --- a/NextcloudTalk/BaseChatTableViewCell+File.swift +++ b/NextcloudTalk/BaseChatTableViewCell+File.swift @@ -321,16 +321,16 @@ extension BaseChatTableViewCell { let fileParameter = self.message?.file(), receivedStatus.fileId == fileParameter.parameterId, receivedStatus.filePath == fileParameter.path, - let progress = userInfo["progress"] as? Float + let progress = userInfo["progress"] as? CGFloat else { return } if self.fileActivityIndicator != nil { // Switch to determinate-mode and show progress self.fileActivityIndicator?.indicatorMode = .determinate - self.fileActivityIndicator?.setProgress(progress, animated: true) + self.fileActivityIndicator?.setProgress(Float(progress), animated: true) } else { // Make sure we have an activity indicator added to this cell - self.addActivityIndicator(with: progress) + self.addActivityIndicator(with: Float(progress)) } } } From 843235738d0ec9e67aad98deb7ecdc4ea6426a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Tue, 19 Mar 2024 18:57:07 +0100 Subject: [PATCH 10/11] Use room capabilities instead of server capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/BaseChatTableViewCell.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/NextcloudTalk/BaseChatTableViewCell.swift b/NextcloudTalk/BaseChatTableViewCell.swift index 0f477891c..38faf0448 100644 --- a/NextcloudTalk/BaseChatTableViewCell.swift +++ b/NextcloudTalk/BaseChatTableViewCell.swift @@ -129,8 +129,6 @@ class BaseChatTableViewCell: UITableViewCell, ReactionsViewDelegate { // swiftlint:disable:next cyclomatic_complexity public func setup(for message: NCChatMessage, withLastCommonReadMessage lastCommonRead: Int) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - self.message = message self.messageId = message.messageId @@ -167,9 +165,13 @@ class BaseChatTableViewCell: UITableViewCell, ReactionsViewDelegate { self.titleLabel.text = actorDisplayName } - let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) - let shouldShowDeliveryStatus = NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityChatReadStatus, forAccountId: activeAccount.accountId) - let shouldShowReadStatus = !serverCapabilities.readStatusPrivacy + guard let room = NCDatabaseManager.sharedInstance().room(withToken: message.token, forAccountId: message.accountId), + let roomCapabilities = NCDatabaseManager.sharedInstance().roomTalkCapabilities(for: room) + else { return } + + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + let shouldShowDeliveryStatus = NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatReadStatus, for: room) + let shouldShowReadStatus = !roomCapabilities.readStatusPrivacy // This check is just a workaround to fix the issue with the deleted parents returned by the API. let parent = message.parent() From a40fa2073971e22192f9699055b78a8309305f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 20 Mar 2024 09:57:41 +0100 Subject: [PATCH 11/11] Adjust minimum cell heights to work with small preferred font sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/BaseChatTableViewCell.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NextcloudTalk/BaseChatTableViewCell.swift b/NextcloudTalk/BaseChatTableViewCell.swift index 38faf0448..dd11211d0 100644 --- a/NextcloudTalk/BaseChatTableViewCell.swift +++ b/NextcloudTalk/BaseChatTableViewCell.swift @@ -33,8 +33,8 @@ protocol BaseChatTableViewCellDelegate: AnyObject { public let chatMessageCellIdentifier = "chatMessageCellIdentifier" public let chatGroupedMessageCellIdentifier = "chatGroupedMessageCellIdentifier" public let chatReplyMessageCellIdentifier = "chatReplyMessageCellIdentifier" -public let chatMessageCellMinimumHeight = 50.0 -public let chatGroupedMessageCellMinimumHeight = 30.0 +public let chatMessageCellMinimumHeight = 45.0 +public let chatGroupedMessageCellMinimumHeight = 25.0 // File cell public let fileMessageCellIdentifier = "fileMessageCellIdentifier"