diff --git a/.github/workflows/uitests.yml b/.github/workflows/uitests.yml index 3808cd1e6..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" \ - | xcpretty + | xcbeautify --quieter - name: Upload test results uses: actions/upload-artifact@v4 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.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"> + + + + + + 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]; 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/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 diff --git a/NextcloudTalk/BaseChatTableViewCell+File.swift b/NextcloudTalk/BaseChatTableViewCell+File.swift new file mode 100644 index 000000000..c2328876c --- /dev/null +++ b/NextcloudTalk/BaseChatTableViewCell+File.swift @@ -0,0 +1,337 @@ +// +// 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() + fileActivityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true + 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? CGFloat + else { return } + + if self.fileActivityIndicator != nil { + // Switch to determinate-mode and show progress + self.fileActivityIndicator?.indicatorMode = .determinate + self.fileActivityIndicator?.setProgress(Float(progress), animated: true) + } else { + // Make sure we have an activity indicator added to this cell + self.addActivityIndicator(with: Float(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..dd11211d0 --- /dev/null +++ b/NextcloudTalk/BaseChatTableViewCell.swift @@ -0,0 +1,504 @@ +// +// 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 = 45.0 +public let chatGroupedMessageCellMinimumHeight = 25.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) { + 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 + } + + 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() + + 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 = [.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 || 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)) + + checkView.image = checkImage + checkView.contentMode = .scaleAspectFit + checkView.tintColor = .systemGray2 + checkView.accessibilityIdentifier = "MessageSent" + checkView.heightAnchor.constraint(equalToConstant: 20).isActive = true + + self.statusView.addArrangedSubview(checkView) + } + } + + // 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..41f41f612 --- /dev/null +++ b/NextcloudTalk/BaseChatTableViewCell.xib @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NextcloudTalk/BaseChatViewController.swift b/NextcloudTalk/BaseChatViewController.swift index 6a9b12537..68d65da37 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 @@ -2700,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) } @@ -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/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/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/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 } 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/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/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/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/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() { 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