";
- };
-/* End PBXGroup section */
-
-/* Begin PBXHeadersBuildPhase section */
- 88916B1F1CF0DF2F00469F91 /* Headers */ = {
- isa = PBXHeadersBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 88916B401CF0DF5100469F91 /* MessageKit.h in Headers */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXHeadersBuildPhase section */
-
-/* Begin PBXNativeTarget section */
- 88916B211CF0DF2F00469F91 /* MessageKit */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 88916B361CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKit" */;
- buildPhases = (
- 88916B1D1CF0DF2F00469F91 /* Sources */,
- 88916B1E1CF0DF2F00469F91 /* Frameworks */,
- 88916B1F1CF0DF2F00469F91 /* Headers */,
- 88916B201CF0DF2F00469F91 /* Resources */,
- B03FF9A51F30398900754FE5 /* ShellScript */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = MessageKit;
- productName = MessageKit;
- productReference = 88916B221CF0DF2F00469F91 /* MessageKit.framework */;
- productType = "com.apple.product-type.framework";
- };
- 88916B2B1CF0DF2F00469F91 /* MessageKitTests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 88916B391CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKitTests" */;
- buildPhases = (
- 88916B281CF0DF2F00469F91 /* Sources */,
- 88916B291CF0DF2F00469F91 /* Frameworks */,
- 88916B2A1CF0DF2F00469F91 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- 88916B2F1CF0DF2F00469F91 /* PBXTargetDependency */,
- );
- name = MessageKitTests;
- productName = MessageKitTests;
- productReference = 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */;
- productType = "com.apple.product-type.bundle.unit-test";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 88916B191CF0DF2F00469F91 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- LastSwiftUpdateCheck = 0730;
- LastUpgradeCheck = 0900;
- ORGANIZATIONNAME = MessageKit;
- TargetAttributes = {
- 88916B211CF0DF2F00469F91 = {
- CreatedOnToolsVersion = 7.3.1;
- LastSwiftMigration = 0800;
- };
- 88916B2B1CF0DF2F00469F91 = {
- CreatedOnToolsVersion = 7.3.1;
- };
- };
- };
- buildConfigurationList = 88916B1C1CF0DF2F00469F91 /* Build configuration list for PBXProject "MessageKit" */;
- compatibilityVersion = "Xcode 3.2";
- developmentRegion = English;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- );
- mainGroup = 88916B181CF0DF2F00469F91;
- productRefGroup = 88916B231CF0DF2F00469F91 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 88916B211CF0DF2F00469F91 /* MessageKit */,
- 88916B2B1CF0DF2F00469F91 /* MessageKitTests */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 88916B201CF0DF2F00469F91 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- B0C8D99B1F73076B000A86E4 /* MessageKitAssets.bundle in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 88916B2A1CF0DF2F00469F91 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- B03FF9A51F30398900754FE5 /* ShellScript */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 88916B1D1CF0DF2F00469F91 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */,
- B06D19331F70D2CA0020E416 /* LocationMessageLayoutDelegate.swift in Sources */,
- B0147C901F5ED0810035B36E /* MessageDateHeaderView.swift in Sources */,
- 38C8679E1F50EA4A00811974 /* InputBarItem.swift in Sources */,
- B01049121F6F86E8008DBA58 /* LocationMessageCell.swift in Sources */,
- B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */,
- 38C8679A1F50EA1000811974 /* NSConstraintLayoutSet.swift in Sources */,
- B06D19351F70D3170020E416 /* MediaMessageLayoutDelegate.swift in Sources */,
- 195F03E11F865BD300066E32 /* MockMessage.swift in Sources */,
- 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */,
- 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */,
- B0147C831F5BE9220035B36E /* Bundle+Extensions.swift in Sources */,
- B06D19371F70D3580020E416 /* LocationMessageSnapshotOptions.swift in Sources */,
- E8586DB51F59A1C300C9BE9D /* MessageContainerView.swift in Sources */,
- B0655A4F1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift in Sources */,
- B0655A2C1F23D81600542A83 /* MessageData.swift in Sources */,
- B0E1756F1F655A1600F0DEF6 /* AvatarHorizontalAlignment.swift in Sources */,
- B01049101F6F8684008DBA58 /* PlayButtonView.swift in Sources */,
- B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */,
- B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */,
- 195F03E31F865C3200066E32 /* MockMessagesDataSource.swift in Sources */,
- B01280F31F4E8798004BCD3E /* MessageLabelDelegate.swift in Sources */,
- B0147C981F61AF930035B36E /* LabelAlignment.swift in Sources */,
- B0655A2A1F23D77200542A83 /* Sender.swift in Sources */,
- B0147C961F600E290035B36E /* MessageKit+Availability.swift in Sources */,
- B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */,
- B0655A4D1F244C0600542A83 /* MessageCollectionViewCell.swift in Sources */,
- B0147C921F6002150035B36E /* MessageKitDateFormatter.swift in Sources */,
- B0655A2E1F23D8BC00542A83 /* MessagesCollectionView.swift in Sources */,
- B0AA1F531F44388C00BAE583 /* NSAttributedString+Extensions.swift in Sources */,
- B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */,
- B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */,
- B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */,
- 38C8679B1F50EA1000811974 /* UIView+Extensions.swift in Sources */,
- B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */,
- 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */,
- D88EBBE01F85252E00F63AD2 /* MockMessagesDisplayDelegate.swift in Sources */,
- B05530B51F493CFA008BB420 /* DetectorType.swift in Sources */,
- B01E2DD81F5BBDB800E4FA1C /* MessageStyle.swift in Sources */,
- 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */,
- B0D943BE1F6DC9AB008B7BFD /* MediaMessageCell.swift in Sources */,
- B0291DA91F6DBB9F00BEDF03 /* TextMessageCell.swift in Sources */,
- B0AA1F511F42E91A00BAE583 /* AvatarAlignment.swift in Sources */,
- B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */,
- 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */,
- B096438E1F2890FB004D0129 /* MessagesDisplayDelegate.swift in Sources */,
- B015E8191F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 88916B281CF0DF2F00469F91 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 1919E61F1F850A6A00DE85BF /* MessageInputBarTests.swift in Sources */,
- 1919E6161F850A3E00DE85BF /* MessageStyleTests.swift in Sources */,
- 1919E6121F850A3B00DE85BF /* AvatarTests.swift in Sources */,
- 1919E61A1F850A6A00DE85BF /* AvatarViewTests.swift in Sources */,
- 1919E61C1F850A6A00DE85BF /* InputTextViewTests.swift in Sources */,
- 1919E6171F850A3E00DE85BF /* SenderTests.swift in Sources */,
- 1919E6141F850A3E00DE85BF /* MessageKitDateFormatterTests.swift in Sources */,
- 1919E61B1F850A6A00DE85BF /* InputBarItemTests.swift in Sources */,
- 195F03DD1F850C7000066E32 /* MessagesViewControllerTests.swift in Sources */,
- 1919E61E1F850A6A00DE85BF /* MessageDateHeaderViewTests.swift in Sources */,
- 1919E6131F850A3E00DE85BF /* DetectorTypeTests.swift in Sources */,
- 1919E61D1F850A6A00DE85BF /* MessageCollectionViewCellTests.swift in Sources */,
- 1919E6151F850A3E00DE85BF /* MessagesDisplayDelegateTests.swift in Sources */,
- 1919E6201F850A6A00DE85BF /* MessagesCollectionViewTests.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 88916B2F1CF0DF2F00469F91 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 88916B211CF0DF2F00469F91 /* MessageKit */;
- targetProxy = 88916B2E1CF0DF2F00469F91 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin XCBuildConfiguration section */
- 88916B341CF0DF2F00469F91 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 4.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- VERSIONING_SYSTEM = "apple-generic";
- VERSION_INFO_PREFIX = "";
- };
- name = Debug;
- };
- 88916B351CF0DF2F00469F91 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SWIFT_VERSION = 4.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- VERSIONING_SYSTEM = "apple-generic";
- VERSION_INFO_PREFIX = "";
- };
- name = Release;
- };
- 88916B371CF0DF2F00469F91 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CLANG_ENABLE_MODULES = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
- DEFINES_MODULE = YES;
- DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 1;
- DYLIB_INSTALL_NAME_BASE = "@rpath";
- INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist";
- INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SKIP_INSTALL = YES;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 4.0;
- };
- name = Debug;
- };
- 88916B381CF0DF2F00469F91 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CLANG_ENABLE_MODULES = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
- DEFINES_MODULE = YES;
- DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 1;
- DYLIB_INSTALL_NAME_BASE = "@rpath";
- INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist";
- INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SKIP_INSTALL = YES;
- SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
- SWIFT_VERSION = 4.0;
- };
- name = Release;
- };
- 88916B3A1CF0DF2F00469F91 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- INFOPLIST_FILE = "Tests/Supporting Files/Info.plist";
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- };
- name = Debug;
- };
- 88916B3B1CF0DF2F00469F91 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- INFOPLIST_FILE = "Tests/Supporting Files/Info.plist";
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 88916B1C1CF0DF2F00469F91 /* Build configuration list for PBXProject "MessageKit" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 88916B341CF0DF2F00469F91 /* Debug */,
- 88916B351CF0DF2F00469F91 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 88916B361CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKit" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 88916B371CF0DF2F00469F91 /* Debug */,
- 88916B381CF0DF2F00469F91 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 88916B391CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKitTests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 88916B3A1CF0DF2F00469F91 /* Debug */,
- 88916B3B1CF0DF2F00469F91 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 88916B191CF0DF2F00469F91 /* Project object */;
-}
diff --git a/MessageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MessageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 3ac286aca..000000000
--- a/MessageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/MessageKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MessageKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d981003..000000000
--- a/MessageKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme
deleted file mode 100644
index 0bb9eb20c..000000000
--- a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme
deleted file mode 100644
index 1569e1b64..000000000
--- a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 000000000..ca0bcbd50
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,47 @@
+// swift-tools-version:6.0
+
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import PackageDescription
+
+let package = Package(
+ name: "MessageKit",
+ platforms: [.iOS(.v14)],
+ products: [
+ .library(name: "MessageKit", targets: ["MessageKit"])
+ ],
+ dependencies: [
+ .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "7.0.0")),
+ ],
+ targets: [
+ // MARK: - MessageKit
+
+ .target(
+ name: "MessageKit",
+ dependencies: ["InputBarAccessoryView"],
+ path: "Sources",
+ exclude: ["Supporting/Info.plist", "Supporting/MessageKit.h"],
+ swiftSettings: [SwiftSetting.define("IS_SPM")]
+ ),
+ .testTarget(name: "MessageKitTests", dependencies: ["MessageKit"]),
+ ],
+ swiftLanguageModes: [.v6]
+)
diff --git a/README.md b/README.md
index 65544b432..260d48555 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,28 @@
-
-
+
+
-
-
+
+ A community-driven replacement for JSQMessagesViewController
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-[](https://circleci.com/gh/MessageKit/MessageKit)
-[](https://codecov.io/gh/MessageKit/MessageKit)
-[](https://github.com/Carthage/Carthage)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
## Goals
@@ -37,65 +32,66 @@
- Provide an awesome Open Source project for the iOS open source community.
- Help others learn.
-## Vision
-See [VISION.md](https://github.com/MessageKit/MessageKit/blob/master/VISION.md) for Goals, Scope, & Technical Considerations.
-
## Installation
-### [CocoaPods](https://cocoapods.org/) **Recommended**
-````ruby
-# Swift 4.2
-pod 'MessageKit'
-````
-> If you are already using Swift 5, use the `3.0.0-swift5` branch until the offical release is made
+### [Swift Package Manager](https://swift.org/package-manager/) - **Recommended**
-### [Carthage](https://github.com/Carthage/Carthage)
+Swift 5.3 in Xcode 12 [added support](https://github.com/apple/swift-evolution/blob/master/proposals/0271-package-manager-resources.md) for assets in Swift Packages.
+You can [just add](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) MessageKit package to your project by entering it's repository URL
-To integrate MessageKit using Carthage, add the following to your `Cartfile`:
-
-````
-github "MessageKit/MessageKit"
-````
+```
+https://github.com/MessageKit/MessageKit
+```
+Older versions of Swift and Xcode don't support MessageKit via SPM.
-### [Manual]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MANUAL_INSTALLATION.md)
+### [Manual](https://github.com/MessageKit/MessageKit/blob/master/Documentation/MANUAL_INSTALLATION.md)
## Requirements
-- **iOS9** or later
-- **Swift 4.2** or later
+- **iOS 14** or later
+- **Swift 6** or later
+
+> For iOS 13 or Swift 5.x please use version 4.3.0
+
+> For iOS 12 or CocoaPods please use version 3.8.0
+
+> For iOS 11 please use version 3.3.0
+> For iOS 9 and iOS 10 please use version 3.1.1
## Getting Started
-### Cell Structure
-
-
-
+Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md).
-Each default cell is a subclass of [`MessageContentCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/MessageContentCell.swift) which has 7 parts. From top down we have a: `cellTopLabel`, `messageTopLabel`, `messageContainerView`, `messageBottomLabel`, `cellBottomLabel` with the `avatarView` and `accessoryView` on either side respectively. Above we see the basic [`TextMessageCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/TextMessageCell.swift) which uses a `MessageLabel` as its main content.
+We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHub with the **"Question"** tag.
-This structure will allow you to create a layout that suits your needs as you can customize the size, appearance and padding of each. If you need something more advanced you can implement a custom cell, which we show how to do in the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project.
+For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md)
+
+Check out the full documentation [here](https://messagekit.github.io/MessageKit/documentation/messagekit).
+
+### Cell Structure
-### MessageInputBar Structure
-
+
-The `MessageInputBar`, derrived from [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView) is a flexible and robust way of creating any kind of input layout you wish. It is self-sizing which means as the user types it will grow to fill available space. It is centered around the `middleContentView` which by default holds the `InputTextView`. This is surrounded by `InputStackView`'s that will also grow in high based on the needs of their subviews `intrinsicContentSize`. See the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project for examples on how to taylor the layout for your own needs.
+Each default cell is a subclass of [`MessageContentCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/MessageContentCell.swift) which has 7 parts. From top down we have a: `cellTopLabel`, `messageTopLabel`, `messageContainerView`, `messageBottomLabel`, `cellBottomLabel` with the `avatarView` and `accessoryView` on either side respectively. Above we see the basic [`TextMessageCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/TextMessageCell.swift) which uses a `MessageLabel` as its main content.
-### Guides
+This structure will allow you to create a layout that suits your needs as you can customize the size, appearance and padding of each. If you need something more advanced you can implement a custom cell, which we show how to do in the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project.
-Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md).
+### InputBarAccessoryView Structure
-We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHib with the **"Question"** tag.
+
+
+
-For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md)
+The `InputBarAccessoryView`, 3rd party dependency from [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView) is a flexible and robust way of creating any kind of input layout you wish. Check the repo and examples there for more info.
## Default Cells
-
-
+
+
The type of cell rendered for a given message is based on the `MessageKind`
@@ -110,6 +106,7 @@ public enum MessageKind {
case emoji(String) // TextMessageCell
case audio(AudioItem) // AudioMessageCell
case contact(ContactItem) // ContactMessageCell
+ case linkPreview(LinkItem) // LinkPreviewMessageCell
/// A custom message.
/// - Note: Using this case requires that you implement the following methods and handle this case:
@@ -120,19 +117,27 @@ public enum MessageKind {
```
If you choose to use the `.custom` kind you are responsible for all of the cells layout. Any `UICollectionViewCell` can be returned for custom cells which means any of the styling you provide from the `MessageDisplayDelegate` will not effect your custom cell. Even if you subclass your cell from `MessageContentCell`.
+[Read more about custom cells](https://github.com/MessageKit/MessageKit/blob/master/Documentation/CUSTOM_CELLS.md)
+[Read more about the cases on the Quick Start guide.](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md#messagekind)
## Contributing
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Tests%22)
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Build+Framework%22)
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22PR+Example+app%22)
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Danger%22)
+
Great! Look over these things first.
+
- Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md)
- Check the [Contributing Guide Lines](https://github.com/MessageKit/MessageKit/blob/master/CONTRIBUTING.md).
-- Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/MjI4NzIzNzMyMzU0LTE1MDMwODIzMDUtYzllYzIyNTU4MA) and π£ don't be a stranger.
-- Check out the [current issues](https://github.com/MessageKit/MessageKit/issues) and see if you can tackle any of those.
-- Download the project and check out the current code base. Suggest any improvements by opening a new issue.
+- Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/zt-2484ymok0-O82~1EtnHALSngQvn6Xwyw) and π£ don't be a stranger.
+- Check out the [current issues](https://github.com/MessageKit/MessageKit/issues) and see if you can tackle any of those.
+- Download the project and check out the current code base. Suggest any improvements by opening a new issue.
- Check out the [What's Next](#whats-next) section :point_down: to see where we are headed.
- Check [StackOverflow](https://stackoverflow.com/questions/tagged/messagekit)
-- Install [SwiftLint](https://github.com/realm/SwiftLint) too keep yourself in :neckbeard: style.
+- Install [SwiftLint](https://github.com/realm/SwiftLint) to keep yourself in :neckbeard: style.
- Be kind and helpful.
@@ -150,26 +155,48 @@ Interested in contributing to MessageKit? Click here to join our [Slack](https:/
Add your app to the list of apps using this library and make a pull request.
+- [ClassDojo](https://www.classdojo.com)
+- [Coursicle](https://apps.apple.com/us/app/coursicle/id1187418307)
+- [Connect Messaging](https://apps.apple.com/app/id1607268774)
+- [Ring4](https://www.ring4.com)
- [Formacar](https://itunes.apple.com/ru/app/id1180117334)
- [HopUp](https://itunes.apple.com/us/app/hopup-airsoft-community/id1128903141?mt=8)
- [MediQuo](https://www.mediquo.com)
- [RappresentaMe](https://itunes.apple.com/it/app/rappresentame/id1330914443)
- [WiseEyes](https://itunes.apple.com/us/app/wiseeyes/id1391408511?mt=8)
-
-*Please provide attribution, it is greatly appreciated.*
+- [SwiftHub](https://github.com/khoren93/SwiftHub)
+- [Studievenn](https://studievenn.no)
+- [SmooveText](https://apps.apple.com/np/app/smoove-text/id1362792811)
+- [COYO Engage](https://apps.apple.com/app/coyo-engage/id1341588804)
+- [HitchPin](https://www.hitchpin.com)
+- [Charge Running](https://apps.apple.com/app/charge-running-live-coaching/id1204578360)
+- [HER](https://apps.apple.com/us/app/id573328837)
+- [Girlfriend Plus](https://apps.apple.com/us/app/girlfriend-plus/id1011637655)
+- [Noon Happen](https://apps.apple.com/app/id1477310602)
+- [XPASS](https://apps.apple.com/cz/app/id1596773834)
+- [HeiaHeia](https://www.heiaheia.com)
+- [Starstruck AI](https://apps.apple.com/au/app/starstruck-message-anyone/id6446234281)
+- [OutyPlay](https://apps.apple.com/app/id6450551793)
+
+_Please provide attribution, it is greatly appreciated._
## Core Team
- [@SD10](https://github.com/sd10), Steven Deutsch
- [@nathantannar4](https://github.com/nathantannar4), Nathan Tannar
- [@zhongwuzw](https://github.com/zhongwuzw), Wu Zhong
+- [@austinwright](https://github.com/austinwright), Austin Wright
+- [@kaspik](https://github.com/kaspik), Jakub Kaspar
+- [@martinpucik](https://github.com/martinpucik), Martin Pucik
## Thanks
Many thanks to [**the contributors**](https://github.com/MessageKit/MessageKit/graphs/contributors) of this project.
## License
+
MessageKit is released under the [MIT License](https://github.com/MessageKit/MessageKit/blob/master/LICENSE.md).
## Inspiration
+
Inspired by [JSQMessagesViewController](https://github.com/jessesquires/JSQMessagesViewController) :point_left: :100:
diff --git a/Scripts/pre-commit b/Scripts/pre-commit
new file mode 100644
index 000000000..ad5572c4d
--- /dev/null
+++ b/Scripts/pre-commit
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+git diff --diff-filter=d --staged --name-only | grep -e '\(.*\).swift$' | while read line; do
+ swift package --allow-writing-to-package-directory format-source-code --file "${line}";
+ git add "$line";
+done
\ No newline at end of file
diff --git a/Sources/Assets.xcassets/Colors/Contents.json b/Sources/Assets.xcassets/Colors/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/avatarViewBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/avatarViewBackground.colorset/Contents.json
new file mode 100644
index 000000000..c3ed2a2e7
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/avatarViewBackground.colorset/Contents.json
@@ -0,0 +1,28 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGrayColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGrayColor"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/collectionViewBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/collectionViewBackground.colorset/Contents.json
new file mode 100644
index 000000000..2afe943ef
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/collectionViewBackground.colorset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBackgroundColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBackgroundColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBackgroundColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBackgroundColor"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/incomingAudioMessageTint.colorset/Contents.json b/Sources/Assets.xcassets/Colors/incomingAudioMessageTint.colorset/Contents.json
new file mode 100644
index 000000000..90a7a7c4a
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/incomingAudioMessageTint.colorset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBlueColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBlueColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBlueColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemBlueColor"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/incomingMessageBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/incomingMessageBackground.colorset/Contents.json
new file mode 100644
index 000000000..093cb4fb6
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/incomingMessageBackground.colorset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray5Color"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray5Color"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray5Color"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray5Color"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/incomingMessageLabel.colorset/Contents.json b/Sources/Assets.xcassets/Colors/incomingMessageLabel.colorset/Contents.json
new file mode 100644
index 000000000..f59aa157e
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/incomingMessageLabel.colorset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "labelColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "labelColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/label.colorset/Contents.json b/Sources/Assets.xcassets/Colors/label.colorset/Contents.json
new file mode 100644
index 000000000..b6604150a
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/label.colorset/Contents.json
@@ -0,0 +1,28 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "labelColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "labelColor"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/outgoingAudioMessageTint.colorset/Contents.json b/Sources/Assets.xcassets/Colors/outgoingAudioMessageTint.colorset/Contents.json
new file mode 100644
index 000000000..39c6f564f
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/outgoingAudioMessageTint.colorset/Contents.json
@@ -0,0 +1,78 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/outgoingMessageBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/outgoingMessageBackground.colorset/Contents.json
new file mode 100644
index 000000000..989aff194
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/outgoingMessageBackground.colorset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGreenColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGreenColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGreenColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGreenColor"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/outgoingMessageLabel.colorset/Contents.json b/Sources/Assets.xcassets/Colors/outgoingMessageLabel.colorset/Contents.json
new file mode 100644
index 000000000..30343633a
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/outgoingMessageLabel.colorset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "labelColor"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "labelColor"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Colors/typingIndicatorDot.colorset/Contents.json b/Sources/Assets.xcassets/Colors/typingIndicatorDot.colorset/Contents.json
new file mode 100644
index 000000000..291cad52a
--- /dev/null
+++ b/Sources/Assets.xcassets/Colors/typingIndicatorDot.colorset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray2Color"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray2Color"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray2Color"
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ },
+ {
+ "appearance" : "contrast",
+ "value" : "high"
+ }
+ ],
+ "color" : {
+ "platform" : "ios",
+ "reference" : "systemGray2Color"
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Contents.json b/Sources/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/Sources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Images/Contents.json b/Sources/Assets.xcassets/Images/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Images/bubble_full.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_full.imageset/Contents.json
new file mode 100644
index 000000000..2ade93161
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/bubble_full.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "bubble_full.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "bubble_full@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "bubble_full@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full.png b/Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full.png
rename to Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full@2x.png b/Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@2x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full@2x.png
rename to Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@2x.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full@3x.png b/Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@3x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full@3x.png
rename to Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@3x.png
diff --git a/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/Contents.json
new file mode 100644
index 000000000..27524111f
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "bubble_full_tail_v1.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "bubble_full_tail_v1@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "bubble_full_tail_v1@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1.png
rename to Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@2x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@2x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@2x.png
rename to Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@2x.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@3x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@3x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@3x.png
rename to Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@3x.png
diff --git a/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/Contents.json
new file mode 100644
index 000000000..7b135c5f4
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "bubble_full_tail_v2.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "bubble_full_tail_v2@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "bubble_full_tail_v2@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2.png
rename to Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@2x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@2x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@2x.png
rename to Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@2x.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@3x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@3x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@3x.png
rename to Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@3x.png
diff --git a/Sources/Assets.xcassets/Images/bubble_outlined.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/Contents.json
new file mode 100644
index 000000000..cb3ef6832
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "bubble_outlined.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "bubble_outlined@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "bubble_outlined@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined.png b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined.png
rename to Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined@2x.png b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@2x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined@2x.png
rename to Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@2x.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined@3x.png b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@3x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined@3x.png
rename to Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@3x.png
diff --git a/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/Contents.json
new file mode 100644
index 000000000..c12e393f9
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "bubble_outlined_tail_v1.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "bubble_outlined_tail_v1@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "bubble_outlined_tail_v1@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1.png
rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@2x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@2x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@2x.png
rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@2x.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@3x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@3x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@3x.png
rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@3x.png
diff --git a/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/Contents.json
new file mode 100644
index 000000000..e2b1619c2
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "bubble_outlined_tail_v2.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "bubble_outlined_tail_v2@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "bubble_outlined_tail_v2@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2.png
rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@2x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@2x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@2x.png
rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@2x.png
diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@3x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@3x.png
similarity index 100%
rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@3x.png
rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@3x.png
diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/Contents.json b/Sources/Assets.xcassets/Images/disclouser.imageset/Contents.json
new file mode 100644
index 000000000..2e16c9978
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/disclouser.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "disclouser.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "disclouser@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "disclouser@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser.png b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser.png
new file mode 100644
index 000000000..1c5a8c9ed
Binary files /dev/null and b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser.png differ
diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@2x.png b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@2x.png
new file mode 100644
index 000000000..8ef8385c5
Binary files /dev/null and b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@2x.png differ
diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@3x.png b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@3x.png
new file mode 100644
index 000000000..cc09ac13f
Binary files /dev/null and b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@3x.png differ
diff --git a/Sources/Assets.xcassets/Images/pause.imageset/Contents.json b/Sources/Assets.xcassets/Images/pause.imageset/Contents.json
new file mode 100644
index 000000000..cb22a98c4
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/pause.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "pause.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "pause@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "pause@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Images/pause.imageset/pause.png b/Sources/Assets.xcassets/Images/pause.imageset/pause.png
new file mode 100644
index 000000000..487a308fb
Binary files /dev/null and b/Sources/Assets.xcassets/Images/pause.imageset/pause.png differ
diff --git a/Sources/Assets.xcassets/Images/pause.imageset/pause@2x.png b/Sources/Assets.xcassets/Images/pause.imageset/pause@2x.png
new file mode 100644
index 000000000..8f5a3c2f4
Binary files /dev/null and b/Sources/Assets.xcassets/Images/pause.imageset/pause@2x.png differ
diff --git a/Sources/Assets.xcassets/Images/pause.imageset/pause@3x.png b/Sources/Assets.xcassets/Images/pause.imageset/pause@3x.png
new file mode 100644
index 000000000..a15cadda6
Binary files /dev/null and b/Sources/Assets.xcassets/Images/pause.imageset/pause@3x.png differ
diff --git a/Sources/Assets.xcassets/Images/play.imageset/Contents.json b/Sources/Assets.xcassets/Images/play.imageset/Contents.json
new file mode 100644
index 000000000..2d3099a40
--- /dev/null
+++ b/Sources/Assets.xcassets/Images/play.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "play.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "play@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "play@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Assets.xcassets/Images/play.imageset/play.png b/Sources/Assets.xcassets/Images/play.imageset/play.png
new file mode 100644
index 000000000..9ae693277
Binary files /dev/null and b/Sources/Assets.xcassets/Images/play.imageset/play.png differ
diff --git a/Sources/Assets.xcassets/Images/play.imageset/play@2x.png b/Sources/Assets.xcassets/Images/play.imageset/play@2x.png
new file mode 100644
index 000000000..c769e68f6
Binary files /dev/null and b/Sources/Assets.xcassets/Images/play.imageset/play@2x.png differ
diff --git a/Sources/Assets.xcassets/Images/play.imageset/play@3x.png b/Sources/Assets.xcassets/Images/play.imageset/play@3x.png
new file mode 100644
index 000000000..53fb82a0d
Binary files /dev/null and b/Sources/Assets.xcassets/Images/play.imageset/play@3x.png differ
diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift
index bdd333e17..974fa1077 100644
--- a/Sources/Controllers/MessagesViewController+Keyboard.swift
+++ b/Sources/Controllers/MessagesViewController+Keyboard.swift
@@ -1,141 +1,152 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Combine
import Foundation
-import MessageInputBar
-
-extension MessagesViewController { // swiftlint:disable:this explicit_acl explicit_top_level_acl
-
- // MARK: - Register / Unregister Observers
-
- internal func addKeyboardObservers() {
- NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: UITextView.textDidBeginEditingNotification, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewTopInset), name: UIDevice.orientationDidChangeNotification, object: nil)
- }
-
- internal func removeKeyboardObservers() {
- NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
- NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: nil)
- NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
- }
-
- // MARK: - Notification Handlers
-
- @objc
- private func handleTextViewDidBeginEditing(_ notification: Notification) {
- if scrollsToBottomOnKeyboardBeginsEditing {
- guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return }
- messagesCollectionView.scrollToBottom(animated: true)
+import InputBarAccessoryView
+import UIKit
+
+extension MessagesViewController {
+ // MARK: Internal
+
+ // MARK: - Register Observers
+
+ internal func addKeyboardObservers() {
+ keyboardManager.bind(
+ inputAccessoryView: inputContainerView,
+ withAdditionalBottomSpace: { [weak self] in self?.inputBarAdditionalBottomSpace() ?? 0 }
+ )
+ keyboardManager.bind(to: messagesCollectionView)
+
+ /// Observe didBeginEditing to scroll content to last item if necessary
+ NotificationCenter.default
+ .publisher(for: UITextView.textDidBeginEditingNotification)
+ .subscribe(on: DispatchQueue.global())
+ /// Wait for inputBar frame change animation to end
+ .delay(for: .milliseconds(200), scheduler: DispatchQueue.main)
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] notification in
+ self?.handleTextViewDidBeginEditing(notification)
+ }
+ .store(in: &disposeBag)
+
+ NotificationCenter.default
+ .publisher(for: UITextView.textDidChangeNotification)
+ .subscribe(on: DispatchQueue.global())
+ .receive(on: DispatchQueue.main)
+ .compactMap { $0.object as? InputTextView }
+ .filter { [weak self] textView in
+ textView == self?.messageInputBar.inputTextView
+ }
+ .map(\.text)
+ .removeDuplicates()
+ .delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
+ .sink { [weak self] _ in
+ self?.updateMessageCollectionViewBottomInset()
+
+ if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
+ self?.messagesCollectionView.scrollToLastItem()
}
+ }
+ .store(in: &disposeBag)
+
+ NotificationCenter.default
+ .publisher(for: UITextInputMode.currentInputModeDidChangeNotification)
+ .subscribe(on: DispatchQueue.global())
+ .receive(on: DispatchQueue.main)
+ .removeDuplicates()
+ .delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
+ .sink { [weak self] _ in
+ self?.updateMessageCollectionViewBottomInset()
+
+ if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
+ self?.messagesCollectionView.scrollToLastItem()
+ }
+ }
+ .store(in: &disposeBag)
+
+ /// Observe frame change of the input bar container to update collectioView bottom inset
+ inputContainerView.publisher(for: \.center)
+ .receive(on: DispatchQueue.main)
+ .removeDuplicates()
+ .sink(receiveValue: { [weak self] _ in
+ self?.updateMessageCollectionViewBottomInset()
+ })
+ .store(in: &disposeBag)
+ }
+
+ // MARK: - Updating insets
+
+ /// Updates bottom messagesCollectionView inset based on the position of inputContainerView
+ internal func updateMessageCollectionViewBottomInset() {
+ let collectionViewHeight = messagesCollectionView.frame.maxY
+ let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) -
+ automaticallyAddedBottomInset
+ let normalizedNewBottomInset = max(0, newBottomInset)
+ let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
+
+ UIView.performWithoutAnimation {
+ guard differenceOfBottomInset != 0 else { return }
+ messagesCollectionView.contentInset.bottom = normalizedNewBottomInset
+ messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
}
-
- @objc
- private func handleKeyboardDidChangeState(_ notification: Notification) {
- guard !isMessagesControllerBeingDismissed else { return }
-
- guard let keyboardStartFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return }
- guard !keyboardStartFrameInScreenCoords.isEmpty else {
- // WORKAROUND for what seems to be a bug in iPad's keyboard handling in iOS 11: we receive an extra spurious frame change
- // notification when undocking the keyboard, with a zero starting frame and an incorrect end frame. The workaround is to
- // ignore this notification.
- return
- }
-
- // Note that the check above does not exclude all notifications from an undocked keyboard, only the weird ones.
- //
- // We've tried following Apple's recommended approach of tracking UIKeyboardWillShow / UIKeyboardDidHide and ignoring frame
- // change notifications while the keyboard is hidden or undocked (undocked keyboard is considered hidden by those events).
- // Unfortunately, we do care about the difference between hidden and undocked, because we have an input bar which is at the
- // bottom when the keyboard is hidden, and is tied to the keyboard when it's undocked.
- //
- // If we follow what Apple recommends and ignore notifications while the keyboard is hidden/undocked, we get an extra inset
- // at the bottom when the undocked keyboard is visible (the inset that tries to compensate for the missing input bar).
- // (Alternatives like setting newBottomInset to 0 or to the height of the input bar don't work either.)
- //
- // We could make it work by adding extra checks for the state of the keyboard and compensating accordingly, but it seems easier
- // to simply check whether the current keyboard frame, whatever it is (even when undocked), covers the bottom of the collection
- // view.
-
- guard let keyboardEndFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
- let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window)
-
- let newBottomInset = requiredScrollViewBottomInset(forKeyboardFrame: keyboardEndFrame)
- let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
-
- if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 {
- let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
- messagesCollectionView.setContentOffset(contentOffset, animated: false)
- }
-
- messageCollectionViewBottomInset = newBottomInset
+ }
+
+ // MARK: Private
+
+ /// UIScrollView can automatically add safe area insets to its contentInset,
+ /// which needs to be accounted for when setting the contentInset based on screen coordinates.
+ ///
+ /// - Returns: The distance automatically added to contentInset.bottom, if any.
+ private var automaticallyAddedBottomInset: CGFloat {
+ messagesCollectionView.adjustedContentInset.bottom - messageCollectionViewBottomInset
+ }
+
+ private var messageCollectionViewBottomInset: CGFloat {
+ messagesCollectionView.contentInset.bottom
+ }
+
+ /// UIScrollView can automatically add safe area insets to its contentInset,
+ /// which needs to be accounted for when setting the contentInset based on screen coordinates.
+ ///
+ /// - Returns: The distance automatically added to contentInset.top, if any.
+ private var automaticallyAddedTopInset: CGFloat {
+ messagesCollectionView.adjustedContentInset.top - messageCollectionViewTopInset
+ }
+
+ private var messageCollectionViewTopInset: CGFloat {
+ messagesCollectionView.contentInset.top
+ }
+
+ // MARK: - Private methods
+
+ private func handleTextViewDidBeginEditing(_ notification: Notification) {
+ guard
+ scrollsToLastItemOnKeyboardBeginsEditing,
+ let inputTextView = notification.object as? InputTextView,
+ inputTextView === messageInputBar.inputTextView
+ else {
+ return
}
-
- // MARK: - Inset Computation
-
- @objc
- internal func adjustScrollViewTopInset() {
- if #available(iOS 11.0, *) {
- // No need to add to the top contentInset
- } else {
- let navigationBarInset = navigationController?.navigationBar.frame.height ?? 0
- let statusBarInset: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : 20
- let topInset = navigationBarInset + statusBarInset
- messagesCollectionView.contentInset.top = topInset
- messagesCollectionView.scrollIndicatorInsets.top = topInset
- }
- }
-
- private func requiredScrollViewBottomInset(forKeyboardFrame keyboardFrame: CGRect) -> CGFloat {
- // we only need to adjust for the part of the keyboard that covers (i.e. intersects) our collection view;
- // see https://developer.apple.com/videos/play/wwdc2017/242/ for more details
- let intersection = messagesCollectionView.frame.intersection(keyboardFrame)
-
- if intersection.isNull || intersection.maxY < messagesCollectionView.frame.maxY {
- // The keyboard is hidden, is a hardware one, or is undocked and does not cover the bottom of the collection view.
- // Note: intersection.maxY may be less than messagesCollectionView.frame.maxY when dealing with undocked keyboards.
- return max(0, additionalBottomInset - automaticallyAddedBottomInset)
- } else {
- return max(0, intersection.height + additionalBottomInset - automaticallyAddedBottomInset)
- }
- }
-
- internal func requiredInitialScrollViewBottomInset() -> CGFloat {
- guard let inputAccessoryView = inputAccessoryView else { return 0 }
- return max(0, inputAccessoryView.frame.height + additionalBottomInset - automaticallyAddedBottomInset)
- }
-
- /// iOS 11's UIScrollView can automatically add safe area insets to its contentInset,
- /// which needs to be accounted for when setting the contentInset based on screen coordinates.
- ///
- /// - Returns: The distance automatically added to contentInset.bottom, if any.
- private var automaticallyAddedBottomInset: CGFloat {
- if #available(iOS 11.0, *) {
- return messagesCollectionView.adjustedContentInset.bottom - messagesCollectionView.contentInset.bottom
- } else {
- return 0
- }
- }
-
+ messagesCollectionView.scrollToLastItem()
+ }
}
diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift
index 361b5ab64..7aa07a36d 100644
--- a/Sources/Controllers/MessagesViewController+Menu.swift
+++ b/Sources/Controllers/MessagesViewController+Menu.swift
@@ -1,100 +1,113 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-import MessageInputBar
+import UIKit
-extension MessagesViewController { // swiftlint:disable:this explicit_acl explicit_top_level_acl
+extension MessagesViewController {
+ // MARK: Internal
- // MARK: - Register / Unregister Observers
+ // MARK: - Register / Unregister Observers
- internal func addMenuControllerObservers() {
- NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: UIMenuController.willShowMenuNotification, object: nil)
- }
-
- internal func removeMenuControllerObservers() {
- NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
- }
-
- // MARK: - Notification Handlers
-
- /// Show menuController and set target rect to selected bubble
- @objc
- private func menuControllerWillShow(_ notification: Notification) {
-
- guard let currentMenuController = notification.object as? UIMenuController,
- let selectedIndexPath = selectedIndexPathForMenu else { return }
-
- NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
- defer {
- NotificationCenter.default.addObserver(self,
- selector: #selector(MessagesViewController.menuControllerWillShow(_:)),
- name: UIMenuController.willShowMenuNotification, object: nil)
- selectedIndexPathForMenu = nil
- }
-
- currentMenuController.setMenuVisible(false, animated: false)
+ internal func addMenuControllerObservers() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(MessagesViewController.menuControllerWillShow(_:)),
+ name: UIMenuController.willShowMenuNotification,
+ object: nil)
+ }
- guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else { return }
- let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view)
+ // MARK: Private
- var messageInputBarFrame: CGRect = .zero
- if let messageInputBarSuperview = messageInputBar.superview {
- messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview)
- }
+ // MARK: - Helpers
- var topNavigationBarFrame: CGRect = navigationBarFrame
- if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview {
- topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview)
- }
-
- let menuHeight = currentMenuController.menuFrame.height
-
- let selectedCellMessageBubblePlusMenuFrame = CGRect(selectedCellMessageBubbleFrame.origin.x, selectedCellMessageBubbleFrame.origin.y - menuHeight, selectedCellMessageBubbleFrame.size.width, selectedCellMessageBubbleFrame.size.height + 2 * menuHeight)
+ private var navigationBarFrame: CGRect {
+ guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else {
+ return .zero
+ }
+ return navigationController.navigationBar.frame
+ }
+
+ // MARK: - Notification Handlers
+
+ /// Show menuController and set target rect to selected bubble
+ @objc
+ private func menuControllerWillShow(_ notification: Notification) {
+ guard
+ let currentMenuController = notification.object as? UIMenuController,
+ let selectedIndexPath = selectedIndexPathForMenu else { return }
+
+ NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
+ defer {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(MessagesViewController.menuControllerWillShow(_:)),
+ name: UIMenuController.willShowMenuNotification,
+ object: nil)
+ selectedIndexPathForMenu = nil
+ }
- var targetRect: CGRect = selectedCellMessageBubbleFrame
- currentMenuController.arrowDirection = .default
+ currentMenuController.hideMenu()
- /// Message bubble intersects with navigationBar and keyboard
- if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) && selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) {
- let centerY = (selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame).minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2
- targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1)
- } /// Message bubble only intersects with navigationBar
- else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) {
- currentMenuController.arrowDirection = .up
- }
+ guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else {
+ return
+ }
+ let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view)
- currentMenuController.setTargetRect(targetRect, in: view)
- currentMenuController.setMenuVisible(true, animated: true)
+ var messageInputBarFrame: CGRect = .zero
+ if let messageInputBarSuperview = messageInputBar.superview {
+ messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview)
}
- // MARK: - Helpers
+ var topNavigationBarFrame: CGRect = navigationBarFrame
+ if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview {
+ topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview)
+ }
- private var navigationBarFrame: CGRect {
- guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else {
- return .zero
- }
- return navigationController.navigationBar.frame
+ let menuHeight = currentMenuController.menuFrame.height
+
+ let selectedCellMessageBubblePlusMenuFrame = CGRect(
+ selectedCellMessageBubbleFrame.origin.x,
+ selectedCellMessageBubbleFrame.origin.y - menuHeight,
+ selectedCellMessageBubbleFrame.size.width,
+ selectedCellMessageBubbleFrame.size.height + 2 * menuHeight)
+
+ var targetRect: CGRect = selectedCellMessageBubbleFrame
+ currentMenuController.arrowDirection = .default
+
+ /// Message bubble intersects with navigationBar and keyboard
+ if
+ selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame),
+ selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame)
+ {
+ let centerY = (
+ selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame)
+ .minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2
+ targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1)
+ } /// Message bubble only intersects with navigationBar
+ else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) {
+ currentMenuController.arrowDirection = .up
}
+
+ currentMenuController.showMenu(from: view, rect: targetRect)
+ }
}
diff --git a/Sources/Controllers/MessagesViewController+PanGesture.swift b/Sources/Controllers/MessagesViewController+PanGesture.swift
new file mode 100644
index 000000000..dc81391d7
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+PanGesture.swift
@@ -0,0 +1,97 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+extension MessagesViewController {
+ // MARK: Internal
+
+ /// Display time of message by swiping the cell
+ func addPanGesture() {
+ panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
+ guard let panGesture = panGesture else {
+ return
+ }
+ panGesture.delegate = self
+ messagesCollectionView.addGestureRecognizer(panGesture)
+ messagesCollectionView.clipsToBounds = false
+ }
+
+ func removePanGesture() {
+ guard let panGesture = panGesture else {
+ return
+ }
+ panGesture.delegate = nil
+ self.panGesture = nil
+ messagesCollectionView.removeGestureRecognizer(panGesture)
+ messagesCollectionView.clipsToBounds = true
+ }
+
+ // MARK: Private
+
+ @objc
+ private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
+ guard let parentView = gesture.view else {
+ return
+ }
+
+ switch gesture.state {
+ case .began, .changed:
+ messagesCollectionView.showsVerticalScrollIndicator = false
+ let translation = gesture.translation(in: view)
+ let minX = -(view.frame.size.width * 0.35)
+ let maxX: CGFloat = 0
+ var offsetValue = translation.x
+ offsetValue = max(offsetValue, minX)
+ offsetValue = min(offsetValue, maxX)
+ parentView.frame.origin.x = offsetValue
+ case .ended:
+ messagesCollectionView.showsVerticalScrollIndicator = true
+ UIView.animate(
+ withDuration: 0.5,
+ delay: 0,
+ usingSpringWithDamping: 0.7,
+ initialSpringVelocity: 0.8,
+ options: .curveEaseOut,
+ animations: {
+ parentView.frame.origin.x = 0
+ },
+ completion: nil)
+ default:
+ break
+ }
+ }
+}
+
+// MARK: - MessagesViewController + UIGestureRecognizerDelegate
+
+extension MessagesViewController: UIGestureRecognizerDelegate {
+ /// Check Pan Gesture Direction:
+ open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
+ return false
+ }
+ let velocity = panGesture.velocity(in: messagesCollectionView)
+ return abs(velocity.x) > abs(velocity.y)
+ }
+}
diff --git a/Sources/Controllers/MessagesViewController+State.swift b/Sources/Controllers/MessagesViewController+State.swift
new file mode 100644
index 000000000..62d2d9cdb
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+State.swift
@@ -0,0 +1,100 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Combine
+import Foundation
+import InputBarAccessoryView
+import UIKit
+
+extension MessagesViewController {
+ @MainActor
+ final class State {
+ /// Pan gesture for display the date of message by swiping left.
+ var panGesture: UIPanGestureRecognizer?
+ var maintainPositionOnInputBarHeightChanged = false
+ var scrollsToLastItemOnKeyboardBeginsEditing = false
+
+ let inputContainerView: MessagesInputContainerView = .init()
+ @Published var inputBarType: MessageInputBarKind = .messageInputBar
+ let keyboardManager = KeyboardManager()
+ var disposeBag: Set = .init()
+ }
+
+ // MARK: - Getters
+
+ public var keyboardManager: KeyboardManager { state.keyboardManager }
+
+ var panGesture: UIPanGestureRecognizer? {
+ get { state.panGesture }
+ set { state.panGesture = newValue }
+ }
+
+ var disposeBag: Set {
+ get { state.disposeBag }
+ set { state.disposeBag = newValue }
+ }
+}
+
+extension MessagesViewController {
+ /// Container holding `messageInputBar` view. To change type of input bar, set `inputBarType` to desired kind.
+ public var inputContainerView: MessagesInputContainerView { state.inputContainerView }
+
+ /// Kind of `messageInputBar` to be added into `inputContainerView`
+ public var inputBarType: MessageInputBarKind {
+ get { state.inputBarType }
+ set { state.inputBarType = newValue }
+ }
+
+ /// A Boolean value that determines whether the `MessagesCollectionView`
+ /// maintains it's current position when the height of the `MessageInputBar` changes.
+ ///
+ /// The default value of this property is `false`.
+ @available(
+ *,
+ deprecated,
+ renamed: "maintainPositionOnInputBarHeightChanged",
+ message: "Please use new property - maintainPositionOnInputBarHeightChanged")
+ public var maintainPositionOnKeyboardFrameChanged: Bool {
+ get { state.maintainPositionOnInputBarHeightChanged }
+ set { state.maintainPositionOnInputBarHeightChanged = newValue }
+ }
+
+ /// A Boolean value that determines whether the `MessagesCollectionView`
+ /// maintains it's current position when the height of the `MessageInputBar` changes.
+ ///
+ /// The default value of this property is `false` and the `MessagesCollectionView` will scroll to bottom after the
+ /// height of the `MessageInputBar` changes.
+ public var maintainPositionOnInputBarHeightChanged: Bool {
+ get { state.maintainPositionOnInputBarHeightChanged }
+ set { state.maintainPositionOnInputBarHeightChanged = newValue }
+ }
+
+ /// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
+ /// last item whenever the `InputTextView` begins editing.
+ ///
+ /// The default value of this property is `false`.
+ /// NOTE: This is related to `scrollToLastItem` whereas the below flag is related to `scrollToBottom` - check each function for differences
+ public var scrollsToLastItemOnKeyboardBeginsEditing: Bool {
+ get { state.scrollsToLastItemOnKeyboardBeginsEditing }
+ set { state.scrollsToLastItemOnKeyboardBeginsEditing = newValue }
+ }
+}
diff --git a/Sources/Controllers/MessagesViewController+TypingIndicator.swift b/Sources/Controllers/MessagesViewController+TypingIndicator.swift
new file mode 100644
index 000000000..434a66bb2
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+TypingIndicator.swift
@@ -0,0 +1,96 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+// MARK: - Typing Indicator API
+
+extension MessagesViewController {
+ // MARK: Open
+
+ /// Sets the typing indicator sate by inserting/deleting the `TypingBubbleCell`
+ ///
+ /// - Parameters:
+ /// - isHidden: A Boolean value that is to be the new state of the typing indicator
+ /// - animated: A Boolean value determining if the insertion is to be animated
+ /// - updates: A block of code that will be executed during `performBatchUpdates`
+ /// when `animated` is `TRUE` or before the `completion` block executes
+ /// when `animated` is `FALSE`
+ /// - completion: A completion block to execute after the insertion/deletion
+ @objc
+ open func setTypingIndicatorViewHidden(
+ _ isHidden: Bool,
+ animated: Bool,
+ whilePerforming updates: (() -> Void)? = nil,
+ completion: ((Bool) -> Void)? = nil)
+ {
+ guard isTypingIndicatorHidden != isHidden else {
+ completion?(false)
+ return
+ }
+
+ let section = messagesCollectionView.numberOfSections
+
+ if animated {
+ messagesCollectionView.performBatchUpdates({ [weak self] in
+ self?.messagesCollectionView.setTypingIndicatorViewHidden(isHidden)
+ self?.performUpdatesForTypingIndicatorVisability(at: section)
+ updates?()
+ }, completion: completion)
+ } else {
+ messagesCollectionView.setTypingIndicatorViewHidden(isHidden)
+ performUpdatesForTypingIndicatorVisability(at: section)
+ updates?()
+ completion?(true)
+ }
+ }
+
+ // MARK: Public
+
+ public var isTypingIndicatorHidden: Bool {
+ messagesCollectionView.isTypingIndicatorHidden
+ }
+
+ /// A method that by default checks if the section is the last in the
+ /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
+ /// is FALSE
+ ///
+ /// - Parameter section
+ /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
+ public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
+ !messagesCollectionView.isTypingIndicatorHidden && section == numberOfSections(in: messagesCollectionView) - 1
+ }
+
+ // MARK: Private
+
+ /// Performs a delete or insert on the `MessagesCollectionView` on the provided section
+ ///
+ /// - Parameter section: The index to modify
+ private func performUpdatesForTypingIndicatorVisability(at section: Int) {
+ if isTypingIndicatorHidden {
+ messagesCollectionView.deleteSections([section - 1])
+ } else {
+ messagesCollectionView.insertSections([section])
+ }
+ }
+}
diff --git a/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift b/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift
new file mode 100644
index 000000000..e524215b4
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift
@@ -0,0 +1,39 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+extension MessagesViewController: UIScrollViewDelegate {
+ open func scrollViewDidScroll(_: UIScrollView) { }
+ open func scrollViewWillBeginDragging(_: UIScrollView) { }
+ open func scrollViewWillEndDragging(
+ _: UIScrollView,
+ withVelocity _: CGPoint,
+ targetContentOffset _: UnsafeMutablePointer) { }
+ open func scrollViewDidEndDragging(_: UIScrollView, willDecelerate _: Bool) { }
+ open func scrollViewWillBeginDecelerating(_: UIScrollView) { }
+ open func scrollViewDidEndDecelerating(_: UIScrollView) { }
+ open func scrollViewDidEndScrollingAnimation(_: UIScrollView) { }
+ open func scrollViewDidScrollToTop(_: UIScrollView) { }
+ open func scrollViewDidChangeAdjustedContentInset(_: UIScrollView) { }
+}
diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift
index ff43ffeb1..41965d5a4 100644
--- a/Sources/Controllers/MessagesViewController.swift
+++ b/Sources/Controllers/MessagesViewController.swift
@@ -1,314 +1,403 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Combine
+import Foundation
+import InputBarAccessoryView
import UIKit
-import MessageInputBar
/// A subclass of `UIViewController` with a `MessagesCollectionView` object
/// that is used to display conversation interfaces.
-open class MessagesViewController: UIViewController,
-UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
-
- /// The `MessagesCollectionView` managed by the messages view controller object.
- open var messagesCollectionView = MessagesCollectionView()
-
- /// The `MessageInputBar` used as the `inputAccessoryView` in the view controller.
- open var messageInputBar = MessageInputBar()
-
- /// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
- /// bottom whenever the `InputTextView` begins editing.
- ///
- /// The default value of this property is `false`.
- open var scrollsToBottomOnKeyboardBeginsEditing: Bool = false
-
- /// A Boolean value that determines whether the `MessagesCollectionView`
- /// maintains it's current position when the height of the `MessageInputBar` changes.
- ///
- /// The default value of this property is `false`.
- open var maintainPositionOnKeyboardFrameChanged: Bool = false
-
- open override var canBecomeFirstResponder: Bool {
- return true
+open class MessagesViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
+ // MARK: Lifecycle
+
+ deinit {
+ NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
+ MessageStyle.bubbleImageCache.removeAllObjects()
+ }
+
+ // MARK: Open
+
+ /// The `MessagesCollectionView` managed by the messages view controller object.
+ open var messagesCollectionView = MessagesCollectionView()
+
+ /// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
+ open lazy var messageInputBar = InputBarAccessoryView()
+
+ /// Display the date of message by swiping left.
+ /// The default value of this property is `false`.
+ open var showMessageTimestampOnSwipeLeft = false {
+ didSet {
+ messagesCollectionView.showMessageTimestampOnSwipeLeft = showMessageTimestampOnSwipeLeft
+ if showMessageTimestampOnSwipeLeft {
+ addPanGesture()
+ } else {
+ removePanGesture()
+ }
}
-
- open override var inputAccessoryView: UIView? {
- return messageInputBar
+ }
+
+ /// A CGFloat value that adds to (or, if negative, subtracts from) the automatically
+ /// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used
+ /// as a measure of last resort when the built-in algorithm does not produce the right
+ /// value for your app. Please let us know when you end up having to use this property.
+ open var additionalBottomInset: CGFloat = 0 {
+ didSet {
+ updateMessageCollectionViewBottomInset()
}
-
- open override var shouldAutorotate: Bool {
- return false
+ }
+
+ /// withAdditionalBottomSpace parameter for InputBarAccessoryView's KeyboardManager
+ open func inputBarAdditionalBottomSpace() -> CGFloat {
+ 0
+ }
+
+ open override func viewDidLoad() {
+ super.viewDidLoad()
+ setupDefaults()
+ setupSubviews()
+ setupConstraints()
+ setupInputBar(for: inputBarType)
+ setupDelegates()
+ addObservers()
+ addKeyboardObservers()
+ addMenuControllerObservers()
+ /// Layout input container view and update messagesCollectionViewInsets
+ view.layoutIfNeeded()
+ }
+
+ open override func viewSafeAreaInsetsDidChange() {
+ super.viewSafeAreaInsetsDidChange()
+ updateMessageCollectionViewBottomInset()
+ }
+
+ // MARK: - UICollectionViewDataSource
+
+ open func numberOfSections(in collectionView: UICollectionView) -> Int {
+ guard let collectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
}
+ let sections = collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0
+ return collectionView.isTypingIndicatorHidden ? sections : sections + 1
+ }
- /// A CGFloat value that adds to (or, if negative, subtracts from) the automatically
- /// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used
- /// as a measure of last resort when the built-in algorithm does not produce the right
- /// value for your app. Please let us know when you end up having to use this property.
- open var additionalBottomInset: CGFloat = 0 {
- didSet {
- let delta = additionalBottomInset - oldValue
- messageCollectionViewBottomInset += delta
- }
+ open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ guard let collectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
+ }
+ if isSectionReservedForTypingIndicator(section) {
+ return 1
+ }
+ return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0
+ }
+
+ /// Notes:
+ /// - If you override this method, remember to call MessagesDataSource's customCell(for:at:in:)
+ /// for MessageKind.custom messages, if necessary.
+ ///
+ /// - If you are using the typing indicator you will need to ensure that the section is not
+ /// reserved for it with `isSectionReservedForTypingIndicator` defined in
+ /// `MessagesCollectionViewFlowLayout`
+ open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
}
- private var isFirstLayout: Bool = true
-
- internal var isMessagesControllerBeingDismissed: Bool = false
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
+ }
- internal var selectedIndexPathForMenu: IndexPath?
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return messagesDataSource.typingIndicator(at: indexPath, in: messagesCollectionView)
+ }
- internal var messageCollectionViewBottomInset: CGFloat = 0 {
- didSet {
- messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset
- messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset
- }
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+
+ switch message.kind {
+ case .text, .attributedText, .emoji:
+ if let cell = messagesDataSource.textCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .photo, .video:
+ if let cell = messagesDataSource.photoCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .location:
+ if let cell = messagesDataSource.locationCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .audio:
+ if let cell = messagesDataSource.audioCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(AudioMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .contact:
+ if let cell = messagesDataSource.contactCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(ContactMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .linkPreview:
+ let cell = messagesCollectionView.dequeueReusableCell(LinkPreviewMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ case .custom:
+ return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView)
+ }
+ }
+
+ open func collectionView(
+ _ collectionView: UICollectionView,
+ viewForSupplementaryElementOfKind kind: String,
+ at indexPath: IndexPath)
+ -> UICollectionReusableView
+ {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
}
- // MARK: - View Life Cycle
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
+ }
- open override func viewDidLoad() {
- super.viewDidLoad()
- setupDefaults()
- setupSubviews()
- setupConstraints()
- setupDelegates()
- addMenuControllerObservers()
- addObservers()
+ switch kind {
+ case UICollectionView.elementKindSectionHeader:
+ return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView)
+ case UICollectionView.elementKindSectionFooter:
+ return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView)
+ default:
+ fatalError(MessageKitError.unrecognizedSectionKind)
+ }
+ }
+
+ // MARK: - UICollectionViewDelegateFlowLayout
+
+ open func collectionView(
+ _: UICollectionView,
+ layout collectionViewLayout: UICollectionViewLayout,
+ sizeForItemAt indexPath: IndexPath)
+ -> CGSize
+ {
+ guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero }
+ return messagesFlowLayout.sizeForItem(at: indexPath)
+ }
+
+ open func collectionView(
+ _ collectionView: UICollectionView,
+ layout _: UICollectionViewLayout,
+ referenceSizeForHeaderInSection section: Int)
+ -> CGSize
+ {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
}
-
- open override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- isMessagesControllerBeingDismissed = false
+ guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
+ fatalError(MessageKitError.nilMessagesLayoutDelegate)
}
-
- open override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- isMessagesControllerBeingDismissed = true
+ if isSectionReservedForTypingIndicator(section) {
+ return .zero
}
-
- open override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- isMessagesControllerBeingDismissed = false
+ return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView)
+ }
+
+ open func collectionView(_: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt _: IndexPath) {
+ guard let cell = cell as? TypingIndicatorCell else { return }
+ cell.typingBubble.startAnimating()
+ }
+
+ open func collectionView(
+ _ collectionView: UICollectionView,
+ layout _: UICollectionViewLayout,
+ referenceSizeForFooterInSection section: Int)
+ -> CGSize
+ {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
}
-
- open override func viewDidLayoutSubviews() {
- // Hack to prevent animation of the contentInset after viewDidAppear
- if isFirstLayout {
- defer { isFirstLayout = false }
- addKeyboardObservers()
- messageCollectionViewBottomInset = requiredInitialScrollViewBottomInset()
- }
- adjustScrollViewTopInset()
+ guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
+ fatalError(MessageKitError.nilMessagesLayoutDelegate)
}
+ if isSectionReservedForTypingIndicator(section) {
+ return .zero
+ }
+ return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView)
+ }
- // MARK: - Initializers
+ open func collectionView(_: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false }
- deinit {
- removeKeyboardObservers()
- removeMenuControllerObservers()
- removeObservers()
- clearMemoryCache()
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return false
}
- // MARK: - Methods [Private]
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
- private func setupDefaults() {
- extendedLayoutIncludesOpaqueBars = true
- automaticallyAdjustsScrollViewInsets = false
- view.backgroundColor = .white
- messagesCollectionView.keyboardDismissMode = .interactive
- messagesCollectionView.alwaysBounceVertical = true
+ switch message.kind {
+ case .text, .attributedText, .emoji, .photo:
+ selectedIndexPathForMenu = indexPath
+ return true
+ default:
+ return false
}
-
- private func setupDelegates() {
- messagesCollectionView.delegate = self
- messagesCollectionView.dataSource = self
+ }
+
+ open func collectionView(
+ _: UICollectionView,
+ canPerformAction action: Selector,
+ forItemAt indexPath: IndexPath,
+ withSender _: Any?)
+ -> Bool
+ {
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return false
}
+ return (action == NSSelectorFromString("copy:"))
+ }
- private func setupSubviews() {
- view.addSubview(messagesCollectionView)
+ open func collectionView(_: UICollectionView, performAction _: Selector, forItemAt indexPath: IndexPath, withSender _: Any?) {
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- private func setupConstraints() {
- messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false
-
- let top = messagesCollectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: topLayoutGuide.length)
- let bottom = messagesCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- if #available(iOS 11.0, *) {
- let leading = messagesCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)
- let trailing = messagesCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
- NSLayoutConstraint.activate([top, bottom, trailing, leading])
- } else {
- let leading = messagesCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
- let trailing = messagesCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
- NSLayoutConstraint.activate([top, bottom, trailing, leading])
- }
+ let pasteBoard = UIPasteboard.general
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+
+ switch message.kind {
+ case .text(let text), .emoji(let text):
+ pasteBoard.string = text
+ case .attributedText(let attributedText):
+ pasteBoard.string = attributedText.string
+ case .photo(let mediaItem):
+ pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage
+ default:
+ break
}
+ }
- // MARK: - UICollectionViewDataSource
+ // MARK: Public
- open func numberOfSections(in collectionView: UICollectionView) -> Int {
- guard let collectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- return collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0
- }
-
- open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
- guard let collectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0
- }
+ public var selectedIndexPathForMenu: IndexPath?
- /// Note:
- /// If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) for MessageKind.custom messages, if necessary
- open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
-
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
-
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
-
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
-
- switch message.kind {
- case .text, .attributedText, .emoji:
- let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .photo, .video:
- let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .location:
- let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .custom:
- return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView)
- }
- }
+ // MARK: Internal
- open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
+ // MARK: - Internal properties
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
+ internal let state: State = .init()
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
+ // MARK: Private
- switch kind {
- case UICollectionView.elementKindSectionHeader:
- return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView)
- case UICollectionView.elementKindSectionFooter:
- return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView)
- default:
- fatalError(MessageKitError.unrecognizedSectionKind)
- }
- }
+ // MARK: - Private methods
- // MARK: - UICollectionViewDelegateFlowLayout
+ private func setupDefaults() {
+ extendedLayoutIncludesOpaqueBars = true
+ view.backgroundColor = .collectionViewBackground
+ messagesCollectionView.keyboardDismissMode = .interactive
+ messagesCollectionView.alwaysBounceVertical = true
+ messagesCollectionView.backgroundColor = .collectionViewBackground
+ }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
- guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero }
- return messagesFlowLayout.sizeForItem(at: indexPath)
- }
+ private func setupSubviews() {
+ view.addSubviews(messagesCollectionView, inputContainerView)
+ }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
+ private func setupConstraints() {
+ messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false
+ /// Constraints of inputContainerView are managed by keyboardManager
+ inputContainerView.translatesAutoresizingMaskIntoConstraints = false
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
- fatalError(MessageKitError.nilMessagesLayoutDelegate)
- }
- return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView)
- }
+ NSLayoutConstraint.activate([
+ messagesCollectionView.topAnchor.constraint(equalTo: view.topAnchor),
+ messagesCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ messagesCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
+ messagesCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
+ ])
+ }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
- fatalError(MessageKitError.nilMessagesLayoutDelegate)
- }
- return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView)
- }
+ private func setupDelegates() {
+ messagesCollectionView.delegate = self
+ messagesCollectionView.dataSource = self
+ }
- open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false }
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
-
- switch message.kind {
- case .text, .attributedText, .emoji, .photo:
- selectedIndexPathForMenu = indexPath
- return true
- default:
- return false
- }
- }
+ private func setupInputBar(for kind: MessageInputBarKind) {
+ inputContainerView.subviews.forEach { $0.removeFromSuperview() }
- open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
- return (action == NSSelectorFromString("copy:"))
- }
+ func pinViewToInputContainer(_ view: UIView) {
+ view.translatesAutoresizingMaskIntoConstraints = false
+ inputContainerView.addSubviews(view)
- open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- let pasteBoard = UIPasteboard.general
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
-
- switch message.kind {
- case .text(let text), .emoji(let text):
- pasteBoard.string = text
- case .attributedText(let attributedText):
- pasteBoard.string = attributedText.string
- case .photo(let mediaItem):
- pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage
- default:
- break
- }
+ NSLayoutConstraint.activate([
+ view.topAnchor.constraint(equalTo: inputContainerView.topAnchor),
+ view.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor),
+ view.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor),
+ view.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor),
+ ])
}
- // MARK: - Helpers
-
- private func addObservers() {
- NotificationCenter.default.addObserver(
- self, selector: #selector(clearMemoryCache), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
- }
-
- private func removeObservers() {
- NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
- }
-
- @objc private func clearMemoryCache() {
- MessageStyle.bubbleImageCache.removeAllObjects()
+ switch kind {
+ case .messageInputBar:
+ pinViewToInputContainer(messageInputBar)
+ case .custom(let view):
+ pinViewToInputContainer(view)
}
+ }
+
+ private func addObservers() {
+ NotificationCenter.default
+ .publisher(for: UIApplication.didReceiveMemoryWarningNotification)
+ .subscribe(on: DispatchQueue.global())
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ self?.clearMemoryCache()
+ }
+ .store(in: &disposeBag)
+
+ state.$inputBarType
+ .subscribe(on: DispatchQueue.global())
+ .dropFirst()
+ .removeDuplicates()
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: { [weak self] newType in
+ self?.setupInputBar(for: newType)
+ })
+ .store(in: &disposeBag)
+ }
+
+ private func clearMemoryCache() {
+ MessageStyle.bubbleImageCache.removeAllObjects()
+ }
}
diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift
index 35dc394dd..aa4ca7d4f 100644
--- a/Sources/Extensions/Bundle+Extensions.swift
+++ b/Sources/Extensions/Bundle+Extensions.swift
@@ -1,43 +1,33 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-internal extension Bundle {
-
- static func messageKitAssetBundle() -> Bundle { // swiftlint:disable:this explicit_acl
- let podBundle = Bundle(for: MessagesViewController.self)
-
- guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else {
- fatalError(MessageKitError.couldNotCreateAssetsPath)
- }
-
- guard let resourceBundle = Bundle(url: resourceBundleUrl) else {
- fatalError(MessageKitError.couldNotLoadAssetsBundle)
- }
-
- return resourceBundle
- }
-
+extension Bundle {
+ #if IS_SPM
+ nonisolated internal static let messageKitAssetBundle = Bundle.module
+ #else
+ nonisolated internal static var messageKitAssetBundle: Bundle {
+ Bundle(for: MessagesViewController.self)
+ }
+ #endif
}
diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift
index 59675a078..2ead04b23 100644
--- a/Sources/Extensions/CGRect+Extensions.swift
+++ b/Sources/Extensions/CGRect+Extensions.swift
@@ -1,33 +1,30 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
-internal extension CGRect {
-
- init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { // swiftlint:disable:this explicit_acl
- self.init(x: x, y: y, width: w, height: h)
- }
-
+extension CGRect {
+ internal init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) {
+ self.init(x: x, y: y, width: w, height: h)
+ }
}
diff --git a/Sources/Extensions/MessageKind+textMessageKind.swift b/Sources/Extensions/MessageKind+textMessageKind.swift
new file mode 100644
index 000000000..bd9a31b21
--- /dev/null
+++ b/Sources/Extensions/MessageKind+textMessageKind.swift
@@ -0,0 +1,36 @@
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+
+extension MessageKind {
+ internal var textMessageKind: MessageKind {
+ switch self {
+ case .linkPreview(let linkItem):
+ return linkItem.textKind
+ case .text, .emoji, .attributedText:
+ return self
+ default:
+ fatalError("textMessageKind not supported for messageKind: \(self)")
+ }
+ }
+}
diff --git a/Sources/Extensions/MessageKit+Availability.swift b/Sources/Extensions/MessageKit+Availability.swift
new file mode 100644
index 000000000..07c051214
--- /dev/null
+++ b/Sources/Extensions/MessageKit+Availability.swift
@@ -0,0 +1,88 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+extension MessagesLayoutDelegate {
+ public func avatarSize(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGSize {
+ fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func avatarPosition(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> AvatarPosition {
+ fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func messageLabelInset(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIEdgeInsets {
+ fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0")
+ }
+
+ public func messagePadding(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIEdgeInsets {
+ fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func cellTopLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment {
+ fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func cellBottomLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment {
+ fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func widthForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat {
+ fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func heightForMedia(
+ message _: MessageType,
+ at _: IndexPath,
+ with _: CGFloat,
+ in _: MessagesCollectionView)
+ -> CGFloat
+ {
+ fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func widthForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ with _: CGFloat,
+ in _: MessagesCollectionView)
+ -> CGFloat
+ {
+ fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func heightForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ with _: CGFloat,
+ in _: MessagesCollectionView)
+ -> CGFloat
+ {
+ fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func shouldCacheLayoutAttributes(for _: MessageType) -> Bool {
+ fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.")
+ }
+}
diff --git a/Sources/Extensions/NSAttributedString+Extensions.swift b/Sources/Extensions/NSAttributedString+Extensions.swift
index 7b49ac65d..87b15671f 100644
--- a/Sources/Extensions/NSAttributedString+Extensions.swift
+++ b/Sources/Extensions/NSAttributedString+Extensions.swift
@@ -1,36 +1,51 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-
-internal extension NSAttributedString {
-
- func width(considering height: CGFloat) -> CGFloat { // swiftlint:disable:this explicit_acl
-
- let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height)
- let rect = self.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
- return rect.width
-
- }
+import UIKit
+
+extension NSAttributedString {
+ public func width(considering height: CGFloat) -> CGFloat {
+ let size = size(consideringHeight: height)
+ return size.width
+ }
+
+ public func height(considering width: CGFloat) -> CGFloat {
+ let size = size(consideringWidth: width)
+ return size.height
+ }
+
+ public func size(consideringHeight height: CGFloat) -> CGSize {
+ let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height)
+ return size(considering: constraintBox)
+ }
+
+ public func size(consideringWidth width: CGFloat) -> CGSize {
+ let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude)
+ return size(considering: constraintBox)
+ }
+
+ public func size(considering size: CGSize) -> CGSize {
+ let rect = boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
+ return rect.size
+ }
}
diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift
index 73f1d357b..e5df7ad80 100644
--- a/Sources/Extensions/UIColor+Extensions.swift
+++ b/Sources/Extensions/UIColor+Extensions.swift
@@ -1,41 +1,58 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
-// swiftlint:disable explicit_acl
+@MainActor
+extension UIColor {
+ // MARK: Internal
-internal extension UIColor {
+ internal static var incomingMessageBackground: UIColor { colorFromAssetBundle(named: "incomingMessageBackground") }
- static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0)
+ internal static var outgoingMessageBackground: UIColor { colorFromAssetBundle(named: "outgoingMessageBackground") }
- static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0)
+ internal static var incomingMessageLabel: UIColor { colorFromAssetBundle(named: "incomingMessageLabel") }
- static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0)
+ internal static var outgoingMessageLabel: UIColor { colorFromAssetBundle(named: "outgoingMessageLabel") }
- static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0)
+ internal static var incomingAudioMessageTint: UIColor { colorFromAssetBundle(named: "incomingAudioMessageTint") }
- static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0)
+ internal static var outgoingAudioMessageTint: UIColor { colorFromAssetBundle(named: "outgoingAudioMessageTint") }
+ internal static var collectionViewBackground: UIColor { colorFromAssetBundle(named: "collectionViewBackground") }
+
+ internal static var typingIndicatorDot: UIColor { colorFromAssetBundle(named: "typingIndicatorDot") }
+
+ internal static var label: UIColor { colorFromAssetBundle(named: "label") }
+
+ internal static var avatarViewBackground: UIColor { colorFromAssetBundle(named: "avatarViewBackground") }
+
+ // MARK: Private
+
+ private static func colorFromAssetBundle(named: String) -> UIColor {
+ guard let color = UIColor(named: named, in: Bundle.messageKitAssetBundle, compatibleWith: nil) else {
+ fatalError(MessageKitError.couldNotFindColorAsset)
+ }
+ return color
+ }
}
diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift
index fc0f9d494..c26740de5 100644
--- a/Sources/Extensions/UIEdgeInsets+Extensions.swift
+++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift
@@ -1,39 +1,34 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
-// swiftlint:disable explicit_acl
-
-internal extension UIEdgeInsets {
-
- var vertical: CGFloat {
- return top + bottom
- }
-
- var horizontal: CGFloat {
- return left + right
- }
+extension UIEdgeInsets {
+ internal var vertical: CGFloat {
+ top + bottom
+ }
+ internal var horizontal: CGFloat {
+ left + right
+ }
}
diff --git a/Sources/Extensions/UIImage+Extensions.swift b/Sources/Extensions/UIImage+Extensions.swift
new file mode 100644
index 000000000..aa371f67e
--- /dev/null
+++ b/Sources/Extensions/UIImage+Extensions.swift
@@ -0,0 +1,38 @@
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import UIKit
+
+// MARK: - ImageType
+
+public enum ImageType: String {
+ case play
+ case pause
+ case disclosure
+}
+
+/// This extension provide a way to access image resources with in framework
+extension UIImage {
+ internal static func messageKitImageWith(type: ImageType) -> UIImage? {
+ UIImage(named: type.rawValue, in: Bundle.messageKitAssetBundle, compatibleWith: nil)
+ }
+}
diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift
index 2fc7a80b9..d741de44b 100644
--- a/Sources/Extensions/UIView+Extensions.swift
+++ b/Sources/Extensions/UIView+Extensions.swift
@@ -1,118 +1,142 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
-// swiftlint:disable explicit_acl
-
-internal extension UIView {
-
- func fillSuperview() {
- guard let superview = self.superview else {
- return
- }
- translatesAutoresizingMaskIntoConstraints = false
-
- let constraints: [NSLayoutConstraint] = [
- leftAnchor.constraint(equalTo: superview.leftAnchor),
- rightAnchor.constraint(equalTo: superview.rightAnchor),
- topAnchor.constraint(equalTo: superview.topAnchor),
- bottomAnchor.constraint(equalTo: superview.bottomAnchor)
- ]
- NSLayoutConstraint.activate(constraints)
+extension UIView {
+ internal func fillSuperview() {
+ guard let superview = superview else {
+ return
}
+ translatesAutoresizingMaskIntoConstraints = false
- func centerInSuperview() {
- guard let superview = self.superview else {
- return
- }
- translatesAutoresizingMaskIntoConstraints = false
- let constraints: [NSLayoutConstraint] = [
- centerXAnchor.constraint(equalTo: superview.centerXAnchor),
- centerYAnchor.constraint(equalTo: superview.centerYAnchor)
- ]
- NSLayoutConstraint.activate(constraints)
+ let constraints: [NSLayoutConstraint] = [
+ leftAnchor.constraint(equalTo: superview.leftAnchor),
+ rightAnchor.constraint(equalTo: superview.rightAnchor),
+ topAnchor.constraint(equalTo: superview.topAnchor),
+ bottomAnchor.constraint(equalTo: superview.bottomAnchor),
+ ]
+ NSLayoutConstraint.activate(constraints)
+ }
+
+ internal func centerInSuperview() {
+ guard let superview = superview else {
+ return
+ }
+ translatesAutoresizingMaskIntoConstraints = false
+ let constraints: [NSLayoutConstraint] = [
+ centerXAnchor.constraint(equalTo: superview.centerXAnchor),
+ centerYAnchor.constraint(equalTo: superview.centerYAnchor),
+ ]
+ NSLayoutConstraint.activate(constraints)
+ }
+
+ internal func constraint(equalTo size: CGSize) {
+ guard superview != nil else { return }
+ translatesAutoresizingMaskIntoConstraints = false
+ let constraints: [NSLayoutConstraint] = [
+ widthAnchor.constraint(equalToConstant: size.width),
+ heightAnchor.constraint(equalToConstant: size.height),
+ ]
+ NSLayoutConstraint.activate(constraints)
+ }
+
+ @discardableResult
+ internal func addConstraints(
+ _ top: NSLayoutYAxisAnchor? = nil,
+ left: NSLayoutXAxisAnchor? = nil,
+ bottom: NSLayoutYAxisAnchor? = nil,
+ right: NSLayoutXAxisAnchor? = nil,
+ centerY: NSLayoutYAxisAnchor? = nil,
+ centerX: NSLayoutXAxisAnchor? = nil,
+ topConstant: CGFloat = 0,
+ leftConstant: CGFloat = 0,
+ bottomConstant: CGFloat = 0,
+ rightConstant: CGFloat = 0,
+ centerYConstant: CGFloat = 0,
+ centerXConstant: CGFloat = 0,
+ widthConstant: CGFloat = 0,
+ heightConstant: CGFloat = 0) -> [NSLayoutConstraint]
+ {
+ if superview == nil {
+ return []
+ }
+ translatesAutoresizingMaskIntoConstraints = false
+
+ var constraints = [NSLayoutConstraint]()
+
+ if let top = top {
+ let constraint = topAnchor.constraint(equalTo: top, constant: topConstant)
+ constraint.identifier = "top"
+ constraints.append(constraint)
+ }
+
+ if let left = left {
+ let constraint = leftAnchor.constraint(equalTo: left, constant: leftConstant)
+ constraint.identifier = "left"
+ constraints.append(constraint)
+ }
+
+ if let bottom = bottom {
+ let constraint = bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant)
+ constraint.identifier = "bottom"
+ constraints.append(constraint)
+ }
+
+ if let right = right {
+ let constraint = rightAnchor.constraint(equalTo: right, constant: -rightConstant)
+ constraint.identifier = "right"
+ constraints.append(constraint)
+ }
+
+ if let centerY = centerY {
+ let constraint = centerYAnchor.constraint(equalTo: centerY, constant: centerYConstant)
+ constraint.identifier = "centerY"
+ constraints.append(constraint)
}
-
- func constraint(equalTo size: CGSize) {
- guard superview != nil else { return }
- translatesAutoresizingMaskIntoConstraints = false
- let constraints: [NSLayoutConstraint] = [
- widthAnchor.constraint(equalToConstant: size.width),
- heightAnchor.constraint(equalToConstant: size.height)
- ]
- NSLayoutConstraint.activate(constraints)
-
+
+ if let centerX = centerX {
+ let constraint = centerXAnchor.constraint(equalTo: centerX, constant: centerXConstant)
+ constraint.identifier = "centerX"
+ constraints.append(constraint)
+ }
+
+ if widthConstant > 0 {
+ let constraint = widthAnchor.constraint(equalToConstant: widthConstant)
+ constraint.identifier = "width"
+ constraints.append(constraint)
}
- @discardableResult
- func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] {
-
- if self.superview == nil {
- return []
- }
- translatesAutoresizingMaskIntoConstraints = false
-
- var constraints = [NSLayoutConstraint]()
-
- if let top = top {
- let constraint = topAnchor.constraint(equalTo: top, constant: topConstant)
- constraint.identifier = "top"
- constraints.append(constraint)
- }
-
- if let left = left {
- let constraint = leftAnchor.constraint(equalTo: left, constant: leftConstant)
- constraint.identifier = "left"
- constraints.append(constraint)
- }
-
- if let bottom = bottom {
- let constraint = bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant)
- constraint.identifier = "bottom"
- constraints.append(constraint)
- }
-
- if let right = right {
- let constraint = rightAnchor.constraint(equalTo: right, constant: -rightConstant)
- constraint.identifier = "right"
- constraints.append(constraint)
- }
-
- if widthConstant > 0 {
- let constraint = widthAnchor.constraint(equalToConstant: widthConstant)
- constraint.identifier = "width"
- constraints.append(constraint)
- }
-
- if heightConstant > 0 {
- let constraint = heightAnchor.constraint(equalToConstant: heightConstant)
- constraint.identifier = "height"
- constraints.append(constraint)
- }
-
- NSLayoutConstraint.activate(constraints)
- return constraints
+ if heightConstant > 0 {
+ let constraint = heightAnchor.constraint(equalToConstant: heightConstant)
+ constraint.identifier = "height"
+ constraints.append(constraint)
}
+
+ NSLayoutConstraint.activate(constraints)
+ return constraints
+ }
+
+ internal func addSubviews(_ subviews: UIView...) {
+ subviews.forEach { addSubview($0) }
+ }
}
diff --git a/Sources/Layout/AudioMessageSizeCalculator.swift b/Sources/Layout/AudioMessageSizeCalculator.swift
new file mode 100644
index 000000000..b08ed2995
--- /dev/null
+++ b/Sources/Layout/AudioMessageSizeCalculator.swift
@@ -0,0 +1,41 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+open class AudioMessageSizeCalculator: MessageSizeCalculator {
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ switch message.kind {
+ case .audio(let item):
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+ if maxWidth < item.size.width {
+ // Maintain the ratio if width is too great
+ let height = maxWidth * item.size.height / item.size.width
+ return CGSize(width: maxWidth, height: height)
+ }
+ return item.size
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
+ }
+ }
+}
diff --git a/Sources/Layout/CellSizeCalculator.swift b/Sources/Layout/CellSizeCalculator.swift
index 7037b5715..85ffbffed 100644
--- a/Sources/Layout/CellSizeCalculator.swift
+++ b/Sources/Layout/CellSizeCalculator.swift
@@ -1,50 +1,53 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// An object is responsible for
/// sizing and configuring cells for given `IndexPath`s.
+@MainActor
open class CellSizeCalculator {
+ // MARK: Lifecycle
- /// The layout object for which the cell size calculator is used.
- public weak var layout: UICollectionViewFlowLayout?
-
- /// Used to configure the layout attributes for a given cell.
- ///
- /// - Parameters:
- /// - attributes: The attributes of the cell.
- /// The default does nothing
- open func configure(attributes: UICollectionViewLayoutAttributes) {}
-
- /// Used to size an item at a given `IndexPath`.
- ///
- /// - Parameters:
- /// - indexPath: The `IndexPath` of the item to be displayed.
- /// The default return .zero
- open func sizeForItem(at indexPath: IndexPath) -> CGSize { return .zero }
-
- public init() {}
+ public init() { }
+ // MARK: Open
+
+ /// Used to configure the layout attributes for a given cell.
+ ///
+ /// - Parameters:
+ /// - attributes: The attributes of the cell.
+ /// The default does nothing
+ open func configure(attributes _: UICollectionViewLayoutAttributes) { }
+
+ /// Used to size an item at a given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - indexPath: The `IndexPath` of the item to be displayed.
+ /// The default return .zero
+ open func sizeForItem(at _: IndexPath) -> CGSize { .zero }
+
+ // MARK: Public
+
+ /// The layout object for which the cell size calculator is used.
+ public weak var layout: UICollectionViewFlowLayout?
}
diff --git a/Sources/Layout/ContactMessageSizeCalculator.swift b/Sources/Layout/ContactMessageSizeCalculator.swift
new file mode 100644
index 000000000..9d2166b29
--- /dev/null
+++ b/Sources/Layout/ContactMessageSizeCalculator.swift
@@ -0,0 +1,76 @@
+// MIT License
+//
+// Copyright (c) 2017-2018 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+open class ContactMessageSizeCalculator: MessageSizeCalculator {
+ // MARK: Open
+
+ open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath)
+ let textInsets = contactLabelInsets(for: message)
+ return maxWidth - textInsets.horizontal
+ }
+
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+
+ var messageContainerSize: CGSize
+ let attributedText: NSAttributedString
+
+ switch message.kind {
+ case .contact(let item):
+ attributedText = NSAttributedString(string: item.displayName, attributes: [.font: contactLabelFont])
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
+ }
+
+ messageContainerSize = labelSize(for: attributedText, considering: maxWidth)
+
+ let messageInsets = contactLabelInsets(for: message)
+ messageContainerSize.width += messageInsets.horizontal
+ messageContainerSize.height += messageInsets.vertical
+
+ return messageContainerSize
+ }
+
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ super.configure(attributes: attributes)
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
+ attributes.messageLabelFont = contactLabelFont
+ }
+
+ // MARK: Public
+
+ public var incomingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 46, bottom: 7, right: 30)
+ public var outgoingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 41, bottom: 7, right: 35)
+ public var contactLabelFont = UIFont.preferredFont(forTextStyle: .body)
+
+ // MARK: Internal
+
+ internal func contactLabelInsets(for message: MessageType) -> UIEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageNameLabelInsets : incomingMessageNameLabelInsets
+ }
+}
diff --git a/Sources/Layout/LinkPreviewMessageSizeCalculator.swift b/Sources/Layout/LinkPreviewMessageSizeCalculator.swift
new file mode 100644
index 000000000..b18f241a0
--- /dev/null
+++ b/Sources/Layout/LinkPreviewMessageSizeCalculator.swift
@@ -0,0 +1,120 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+// MARK: - LinkPreviewMessageSizeCalculator
+
+open class LinkPreviewMessageSizeCalculator: TextMessageSizeCalculator {
+ // MARK: Lifecycle
+
+ public override init(layout: MessagesCollectionViewFlowLayout?) {
+ let titleFont = UIFont.systemFont(ofSize: 13, weight: .semibold)
+ let titleFontMetrics = UIFontMetrics(forTextStyle: .footnote)
+ self.titleFont = titleFontMetrics.scaledFont(for: titleFont)
+
+ let domainFont = UIFont.systemFont(ofSize: 12, weight: .semibold)
+ let domainFontMetrics = UIFontMetrics(forTextStyle: .caption1)
+ self.domainFont = domainFontMetrics.scaledFont(for: domainFont)
+
+ super.init(layout: layout)
+ }
+
+ // MARK: Open
+
+ open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ switch message.kind {
+ case .linkPreview:
+ let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath)
+ return max(maxWidth, (layout?.collectionView?.bounds.width ?? 0) * 0.75)
+ default:
+ return super.messageContainerMaxWidth(for: message, at: indexPath)
+ }
+ }
+
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ guard case MessageKind.linkPreview(let linkItem) = message.kind else {
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
+ }
+
+ var containerSize = super.messageContainerSize(for: message, at: indexPath)
+ containerSize.width = max(containerSize.width, messageContainerMaxWidth(for: message, at: indexPath))
+
+ let labelInsets: UIEdgeInsets = messageLabelInsets(for: message)
+
+ let minHeight = containerSize.height + LinkPreviewMessageSizeCalculator.imageViewSize
+ let previewMaxWidth = containerSize
+ .width -
+ (
+ LinkPreviewMessageSizeCalculator.imageViewSize + LinkPreviewMessageSizeCalculator.imageViewMargin + labelInsets
+ .horizontal)
+
+ calculateContainerSize(
+ with: NSAttributedString(string: linkItem.title ?? "", attributes: [.font: titleFont]),
+ containerSize: &containerSize,
+ maxWidth: previewMaxWidth)
+
+ calculateContainerSize(
+ with: NSAttributedString(string: linkItem.teaser, attributes: [.font: teaserFont]),
+ containerSize: &containerSize,
+ maxWidth: previewMaxWidth)
+
+ calculateContainerSize(
+ with: NSAttributedString(string: linkItem.url.host ?? "", attributes: [.font: domainFont]),
+ containerSize: &containerSize,
+ maxWidth: previewMaxWidth)
+
+ containerSize.height = max(minHeight, containerSize.height) + labelInsets.vertical
+
+ return containerSize
+ }
+
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ super.configure(attributes: attributes)
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
+ attributes.linkPreviewFonts = LinkPreviewFonts(titleFont: titleFont, teaserFont: teaserFont, domainFont: domainFont)
+ }
+
+ // MARK: Public
+
+ public var titleFont: UIFont
+ public var teaserFont: UIFont = .preferredFont(forTextStyle: .caption2)
+ public var domainFont: UIFont
+
+ // MARK: Internal
+
+ static let imageViewSize: CGFloat = 60
+ static let imageViewMargin: CGFloat = 8
+}
+
+extension LinkPreviewMessageSizeCalculator {
+ private func calculateContainerSize(
+ with attributedString: NSAttributedString,
+ containerSize: inout CGSize,
+ maxWidth: CGFloat)
+ {
+ guard !attributedString.string.isEmpty else { return }
+ let size = labelSize(for: attributedString, considering: maxWidth)
+ containerSize.height += size.height
+ }
+}
diff --git a/Sources/Layout/LocationMessageSizeCalculator.swift b/Sources/Layout/LocationMessageSizeCalculator.swift
index 0efc0260b..56ce3329c 100644
--- a/Sources/Layout/LocationMessageSizeCalculator.swift
+++ b/Sources/Layout/LocationMessageSizeCalculator.swift
@@ -1,43 +1,41 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
open class LocationMessageSizeCalculator: MessageSizeCalculator {
-
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- switch message.kind {
- case .location(let item):
- let maxWidth = messageContainerMaxWidth(for: message)
- if maxWidth < item.size.width {
- // Maintain the ratio if width is too great
- let height = maxWidth * item.size.height / item.size.width
- return CGSize(width: maxWidth, height: height)
- }
- return item.size
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ switch message.kind {
+ case .location(let item):
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+ if maxWidth < item.size.width {
+ // Maintain the ratio if width is too great
+ let height = maxWidth * item.size.height / item.size.width
+ return CGSize(width: maxWidth, height: height)
+ }
+ return item.size
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
}
+ }
}
diff --git a/Sources/Layout/MediaMessageSizeCalculator.swift b/Sources/Layout/MediaMessageSizeCalculator.swift
index 020bbd624..c6eebaf91 100644
--- a/Sources/Layout/MediaMessageSizeCalculator.swift
+++ b/Sources/Layout/MediaMessageSizeCalculator.swift
@@ -1,48 +1,46 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
open class MediaMessageSizeCalculator: MessageSizeCalculator {
-
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- let maxWidth = messageContainerMaxWidth(for: message)
- let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in
- if maxWidth < item.size.width {
- // Maintain the ratio if width is too great
- let height = maxWidth * item.size.height / item.size.width
- return CGSize(width: maxWidth, height: height)
- }
- return item.size
- }
- switch message.kind {
- case .photo(let item):
- return sizeForMediaItem(maxWidth, item)
- case .video(let item):
- return sizeForMediaItem(maxWidth, item)
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+ let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in
+ if maxWidth < item.size.width {
+ // Maintain the ratio if width is too great
+ let height = maxWidth * item.size.height / item.size.width
+ return CGSize(width: maxWidth, height: height)
+ }
+ return item.size
+ }
+ switch message.kind {
+ case .photo(let item):
+ return sizeForMediaItem(maxWidth, item)
+ case .video(let item):
+ return sizeForMediaItem(maxWidth, item)
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
}
+ }
}
diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift
index f83f826e9..099487215 100644
--- a/Sources/Layout/MessageSizeCalculator.swift
+++ b/Sources/Layout/MessageSizeCalculator.swift
@@ -1,256 +1,353 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
-open class MessageSizeCalculator: CellSizeCalculator {
+// MARK: - MessageSizeCalculator
- public init(layout: MessagesCollectionViewFlowLayout? = nil) {
- super.init()
-
- self.layout = layout
+open class MessageSizeCalculator: CellSizeCalculator {
+ // MARK: Lifecycle
+
+ public init(layout: MessagesCollectionViewFlowLayout? = nil) {
+ super.init()
+
+ self.layout = layout
+ }
+
+ // MARK: Open
+
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
+
+ let dataSource = messagesLayout.messagesDataSource
+ let indexPath = attributes.indexPath
+ let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
+
+ attributes.avatarSize = avatarSize(for: message, at: indexPath)
+ attributes.avatarPosition = avatarPosition(for: message)
+ attributes.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding
+
+ attributes.messageContainerPadding = messageContainerPadding(for: message)
+ attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath)
+ attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath)
+ attributes.cellTopLabelAlignment = cellTopLabelAlignment(for: message)
+ attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath)
+ attributes.messageTimeLabelSize = messageTimeLabelSize(for: message, at: indexPath)
+ attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: message)
+ attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath)
+ attributes.messageTopLabelAlignment = messageTopLabelAlignment(for: message, at: indexPath)
+
+ attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message, at: indexPath)
+ attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath)
+
+ attributes.accessoryViewSize = accessoryViewSize(for: message)
+ attributes.accessoryViewPadding = accessoryViewPadding(for: message)
+ attributes.accessoryViewPosition = accessoryViewPosition(for: message)
+ }
+
+ open override func sizeForItem(at indexPath: IndexPath) -> CGSize {
+ let dataSource = messagesLayout.messagesDataSource
+ let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
+ let itemHeight = cellContentHeight(for: message, at: indexPath)
+ return CGSize(width: messagesLayout.itemWidth, height: itemHeight)
+ }
+
+ open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let messageContainerHeight = messageContainerSize(for: message, at: indexPath).height
+ let cellBottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height
+ let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height
+ let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height
+ let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height
+ let messageVerticalPadding = messageContainerPadding(for: message).vertical
+ let avatarHeight = avatarSize(for: message, at: indexPath).height
+ let avatarVerticalPosition = avatarPosition(for: message).vertical
+ let accessoryViewHeight = accessoryViewSize(for: message).height
+
+ switch avatarVerticalPosition {
+ case .messageCenter:
+ let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
+ + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
+ let cellHeight = max(avatarHeight, totalLabelHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .messageBottom:
+ var cellHeight: CGFloat = 0
+ cellHeight += messageBottomLabelHeight
+ cellHeight += cellBottomLabelHeight
+ let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight
+ cellHeight += max(labelsHeight, avatarHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .messageTop:
+ var cellHeight: CGFloat = 0
+ cellHeight += cellTopLabelHeight
+ cellHeight += messageTopLabelHeight
+ let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
+ cellHeight += max(labelsHeight, avatarHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .messageLabelTop:
+ var cellHeight: CGFloat = 0
+ cellHeight += cellTopLabelHeight
+ let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding +
+ messageTopLabelHeight + cellBottomLabelHeight
+ cellHeight += max(messageLabelsHeight, avatarHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .cellTop, .cellBottom:
+ let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
+ + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
+ let cellHeight = max(avatarHeight, totalLabelHeight)
+ return max(cellHeight, accessoryViewHeight)
}
+ }
- public var incomingAvatarSize = CGSize(width: 30, height: 30)
- public var outgoingAvatarSize = CGSize(width: 30, height: 30)
-
- public var incomingAvatarPosition = AvatarPosition(vertical: .cellBottom)
- public var outgoingAvatarPosition = AvatarPosition(vertical: .cellBottom)
-
- public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30)
- public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)
-
- public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
-
- public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
- public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
-
- public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
- public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
-
- public var incomingAccessoryViewSize = CGSize.zero
- public var outgoingAccessoryViewSize = CGSize.zero
+ // MARK: - Avatar
- public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero
- public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero
+ open func avatarPosition(for message: MessageType) -> AvatarPosition {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition
- open override func configure(attributes: UICollectionViewLayoutAttributes) {
- guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
-
- let dataSource = messagesLayout.messagesDataSource
- let indexPath = attributes.indexPath
- let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
-
- attributes.avatarSize = avatarSize(for: message)
- attributes.avatarPosition = avatarPosition(for: message)
-
- attributes.messageContainerPadding = messageContainerPadding(for: message)
- attributes.messageContainerSize = messageContainerSize(for: message)
- attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath)
- attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath)
- attributes.messageTopLabelAlignment = messageTopLabelAlignment(for: message)
-
- attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message)
- attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath)
-
- attributes.accessoryViewSize = accessoryViewSize(for: message)
- attributes.accessoryViewPadding = accessoryViewPadding(for: message)
+ switch position.horizontal {
+ case .cellTrailing, .cellLeading:
+ break
+ case .natural:
+ position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading
}
-
- open override func sizeForItem(at indexPath: IndexPath) -> CGSize {
- let dataSource = messagesLayout.messagesDataSource
- let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
- let itemHeight = cellContentHeight(for: message, at: indexPath)
- return CGSize(width: messagesLayout.itemWidth, height: itemHeight)
+ return position
+ }
+
+ open func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ if let size = layoutDelegate.avatarSize(for: message, at: indexPath, in: collectionView) {
+ return size
}
-
- open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
-
- let messageContainerHeight = messageContainerSize(for: message).height
- let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height
- let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height
- let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height
- let messageVerticalPadding = messageContainerPadding(for: message).vertical
- let avatarHeight = avatarSize(for: message).height
- let avatarVerticalPosition = avatarPosition(for: message).vertical
- let accessoryViewHeight = accessoryViewSize(for: message).height
-
- switch avatarVerticalPosition {
- case .messageCenter:
- let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
- + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight
- let cellHeight = max(avatarHeight, totalLabelHeight)
- return max(cellHeight, accessoryViewHeight)
- case .messageBottom:
- var cellHeight: CGFloat = 0
- cellHeight += messageBottomLabelHeight
- let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight
- cellHeight += max(labelsHeight, avatarHeight)
- return max(cellHeight, accessoryViewHeight)
- case .messageTop:
- var cellHeight: CGFloat = 0
- cellHeight += cellTopLabelHeight
- cellHeight += messageTopLabelHeight
- let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight
- cellHeight += max(labelsHeight, avatarHeight)
- return max(cellHeight, accessoryViewHeight)
- case .messageLabelTop:
- var cellHeight: CGFloat = 0
- cellHeight += cellTopLabelHeight
- let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding + messageTopLabelHeight
- cellHeight += max(messageLabelsHeight, avatarHeight)
- return max(cellHeight, accessoryViewHeight)
- case .cellTop, .cellBottom:
- let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
- + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight
- let cellHeight = max(avatarHeight, totalLabelHeight)
- return max(cellHeight, accessoryViewHeight)
- }
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize
+ }
+
+ // MARK: - Top cell Label
+
+ open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment
+ }
+
+ // MARK: - Top message Label
+
+ open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func messageTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment {
+ let collectionView = messagesLayout.messagesCollectionView
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+
+ if let alignment = layoutDelegate.messageTopLabelAlignment(for: message, at: indexPath, in: collectionView) {
+ return alignment
}
- // MARK: - Avatar
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment
+ }
- open func avatarPosition(for message: MessageType) -> AvatarPosition {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition
+ // MARK: - Message time label
- switch position.horizontal {
- case .cellTrailing, .cellLeading:
- break
- case .natural:
- position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading
- }
- return position
+ open func messageTimeLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let dataSource = messagesLayout.messagesDataSource
+ guard let attributedText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath) else {
+ return .zero
}
-
- open func avatarSize(for message: MessageType) -> CGSize {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize
+ let size = attributedText.size()
+ return CGSize(width: size.width, height: size.height)
+ }
+
+ // MARK: - Bottom cell Label
+
+ open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.cellBottomLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment
+ }
+
+ // MARK: - Bottom Message Label
+
+ open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func messageBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment {
+ let collectionView = messagesLayout.messagesCollectionView
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+
+ if let alignment = layoutDelegate.messageBottomLabelAlignment(for: message, at: indexPath, in: collectionView) {
+ return alignment
}
- // MARK: - Top cell Label
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment
+ }
- open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let layoutDelegate = messagesLayout.messagesLayoutDelegate
- let collectionView = messagesLayout.messagesCollectionView
- let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView)
- return CGSize(width: messagesLayout.itemWidth, height: height)
- }
+ // MARK: - MessageContainer
- open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment
- }
-
- // MARK: - Top message Label
-
- open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let layoutDelegate = messagesLayout.messagesLayoutDelegate
- let collectionView = messagesLayout.messagesCollectionView
- let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView)
- return CGSize(width: messagesLayout.itemWidth, height: height)
- }
-
- open func messageTopLabelAlignment(for message: MessageType) -> LabelAlignment {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment
- }
+ open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding
+ }
- // MARK: - Bottom Label
+ open func messageContainerSize(for _: MessageType, at _: IndexPath) -> CGSize {
+ // Returns .zero by default
+ .zero
+ }
- open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let layoutDelegate = messagesLayout.messagesLayoutDelegate
- let collectionView = messagesLayout.messagesCollectionView
- let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView)
- return CGSize(width: messagesLayout.itemWidth, height: height)
- }
+ open func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let avatarWidth: CGFloat = avatarSize(for: message, at: indexPath).width
+ let messagePadding = messageContainerPadding(for: message)
+ let accessoryWidth = accessoryViewSize(for: message).width
+ let accessoryPadding = accessoryViewPadding(for: message)
+ return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal - accessoryWidth - accessoryPadding
+ .horizontal - avatarLeadingTrailingPadding
+ }
- open func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment
- }
+ // MARK: Public
- // MARK: - Accessory View
+ public var incomingAvatarSize = CGSize(width: 30, height: 30)
+ public var outgoingAvatarSize = CGSize(width: 30, height: 30)
- public func accessoryViewSize(for message: MessageType) -> CGSize {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize
- }
+ public var incomingAvatarPosition = AvatarPosition(vertical: .cellBottom)
+ public var outgoingAvatarPosition = AvatarPosition(vertical: .cellBottom)
- public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding
- }
+ public var avatarLeadingTrailingPadding: CGFloat = 0
- // MARK: - MessageContainer
+ public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30)
+ public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)
- open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding
- }
+ public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- open func messageContainerSize(for message: MessageType) -> CGSize {
- // Returns .zero by default
- return .zero
- }
+ public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
+ public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
- open func messageContainerMaxWidth(for message: MessageType) -> CGFloat {
- let avatarWidth = avatarSize(for: message).width
- let messagePadding = messageContainerPadding(for: message)
- let accessoryWidth = accessoryViewSize(for: message).width
- let accessoryPadding = accessoryViewPadding(for: message)
- return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal - accessoryWidth - accessoryPadding.horizontal
- }
+ public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
+ public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
- // MARK: - Helpers
+ public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
+ public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
- public var messagesLayout: MessagesCollectionViewFlowLayout {
- guard let layout = layout as? MessagesCollectionViewFlowLayout else {
- fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout")
- }
- return layout
- }
+ public var incomingAccessoryViewSize = CGSize.zero
+ public var outgoingAccessoryViewSize = CGSize.zero
- internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize {
- let constraintBox = CGSize(width: maxWidth, height: .greatestFiniteMagnitude)
- let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral
+ public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero
+ public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero
- return rect.size
+ public var incomingAccessoryViewPosition: AccessoryPosition = .messageCenter
+ public var outgoingAccessoryViewPosition: AccessoryPosition = .messageCenter
+
+ // MARK: - Helpers
+
+ public var messagesLayout: MessagesCollectionViewFlowLayout {
+ guard let layout = layout as? MessagesCollectionViewFlowLayout else {
+ fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout")
}
+ return layout
+ }
+
+ // MARK: - Accessory View
+
+ public func accessoryViewSize(for message: MessageType) -> CGSize {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize
+ }
+
+ public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding
+ }
+
+ public func accessoryViewPosition(for message: MessageType) -> AccessoryPosition {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAccessoryViewPosition : incomingAccessoryViewPosition
+ }
+
+ // MARK: Internal
+ internal lazy var textContainer: NSTextContainer = {
+ let textContainer = NSTextContainer()
+ textContainer.maximumNumberOfLines = 0
+ textContainer.lineFragmentPadding = 0
+ return textContainer
+ }()
+ internal lazy var layoutManager: NSLayoutManager = {
+ let layoutManager = NSLayoutManager()
+ layoutManager.addTextContainer(textContainer)
+ return layoutManager
+ }()
+ internal lazy var textStorage: NSTextStorage = {
+ let textStorage = NSTextStorage()
+ textStorage.addLayoutManager(layoutManager)
+ return textStorage
+ }()
+
+ internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize {
+ let constraintBox = CGSize(width: maxWidth, height: .greatestFiniteMagnitude)
+
+ textContainer.size = constraintBox
+ textStorage.replaceCharacters(in: NSRange(location: 0, length: textStorage.length), with: attributedText)
+ layoutManager.ensureLayout(for: textContainer)
+
+ let size = layoutManager.usedRect(for: textContainer).size
+
+ return CGSize(width: size.width.rounded(.up), height: size.height.rounded(.up))
+ }
}
-fileprivate extension UIEdgeInsets {
- init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
- self.init(top: top, left: left, bottom: bottom, right: right)
- }
+extension UIEdgeInsets {
+ fileprivate init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
+ self.init(top: top, left: left, bottom: bottom, right: right)
+ }
}
diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift
index cd5fe5af8..fa280b932 100644
--- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift
+++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift
@@ -1,262 +1,357 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import AVFoundation
+import Foundation
+import UIKit
/// The layout object used by `MessagesCollectionView` to determine the size of all
/// framework provided `MessageCollectionViewCell` subclasses.
open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
+ // MARK: Lifecycle
+
+ // MARK: - Initializers
+
+ public override init() {
+ super.init()
+ setupView()
+ setupObserver()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupView()
+ setupObserver()
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ // MARK: Open
+
+ open override class var layoutAttributesClass: AnyClass {
+ MessagesCollectionViewLayoutAttributes.self
+ }
- open override class var layoutAttributesClass: AnyClass {
- return MessagesCollectionViewLayoutAttributes.self
+ // MARK: - Cell Sizing
+
+ lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
+ lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
+ lazy open var emojiMessageSizeCalculator: TextMessageSizeCalculator = {
+ let sizeCalculator = TextMessageSizeCalculator(layout: self)
+ sizeCalculator.messageLabelFont = UIFont.systemFont(ofSize: sizeCalculator.messageLabelFont.pointSize * 2)
+ return sizeCalculator
+ }()
+
+ lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
+ lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
+ lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self)
+ lazy open var audioMessageSizeCalculator = AudioMessageSizeCalculator(layout: self)
+ lazy open var contactMessageSizeCalculator = ContactMessageSizeCalculator(layout: self)
+ lazy open var typingIndicatorSizeCalculator = TypingCellSizeCalculator(layout: self)
+ lazy open var linkPreviewMessageSizeCalculator = LinkPreviewMessageSizeCalculator(layout: self)
+
+ /// A method that by default checks if the section is the last in the
+ /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
+ /// is FALSE
+ ///
+ /// - Parameter section
+ /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
+ open func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
+ !isTypingIndicatorViewHidden && section == messagesCollectionView.numberOfSections - 1
+ }
+
+ // MARK: - Attributes
+
+ open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
+ guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes]
+ else {
+ return nil
}
-
- /// The `MessagesCollectionView` that owns this layout object.
- public var messagesCollectionView: MessagesCollectionView {
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.layoutUsedOnForeignType)
- }
- return messagesCollectionView
+ for attributes in attributesArray where attributes.representedElementCategory == .cell {
+ let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
+ cellSizeCalculator.configure(attributes: attributes)
}
-
- /// The `MessagesDataSource` for the layout's collection view.
- public var messagesDataSource: MessagesDataSource {
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- return messagesDataSource
+ return attributesArray
+ }
+
+ open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
+ guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else {
+ return nil
}
-
- /// The `MessagesLayoutDelegate` for the layout's collection view.
- public var messagesLayoutDelegate: MessagesLayoutDelegate {
- guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
- fatalError(MessageKitError.nilMessagesLayoutDelegate)
- }
- return messagesLayoutDelegate
+ if attributes.representedElementCategory == .cell {
+ let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
+ cellSizeCalculator.configure(attributes: attributes)
}
+ return attributes
+ }
- public var itemWidth: CGFloat {
- guard let collectionView = collectionView else { return 0 }
- return collectionView.frame.width - sectionInset.left - sectionInset.right
- }
+ // MARK: - Layout Invalidation
- // MARK: - Initializers
+ open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
+ collectionView?.bounds.width != newBounds.width
+ }
- public override init() {
- super.init()
-
- setupView()
- setupObserver()
- }
+ open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
+ let context = super.invalidationContext(forBoundsChange: newBounds)
+ guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context }
+ flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds)
+ return flowLayoutContext
+ }
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
-
- setupView()
- setupObserver()
- }
+ /// Note:
+ /// - If you override this method, remember to call MessageLayoutDelegate's
+ /// customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary
+ /// - If you are using the typing indicator be sure to return the `typingIndicatorSizeCalculator`
+ /// when the section is reserved for it, indicated by `isSectionReservedForTypingIndicator`
+ open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return typingIndicatorSizeCalculator
+ }
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+ switch message.kind {
+ case .text:
+ return messagesLayoutDelegate
+ .textCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? textMessageSizeCalculator
+ case .attributedText:
+ return messagesLayoutDelegate.attributedTextCellSizeCalculator(
+ for: message,
+ at: indexPath,
+ in: messagesCollectionView) ?? attributedTextMessageSizeCalculator
+ case .emoji:
+ return messagesLayoutDelegate
+ .emojiCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? emojiMessageSizeCalculator
+ case .photo:
+ return messagesLayoutDelegate
+ .photoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? photoMessageSizeCalculator
+ case .video:
+ return messagesLayoutDelegate
+ .videoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? videoMessageSizeCalculator
+ case .location:
+ return messagesLayoutDelegate
+ .locationCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ??
+ locationMessageSizeCalculator
+ case .audio:
+ return messagesLayoutDelegate
+ .audioCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? audioMessageSizeCalculator
+ case .contact:
+ return messagesLayoutDelegate
+ .contactCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? contactMessageSizeCalculator
+ case .linkPreview:
+ return linkPreviewMessageSizeCalculator
+ case .custom:
+ return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView)
+ }
+ }
- deinit {
- NotificationCenter.default.removeObserver(self)
- }
-
- // MARK: - Methods
-
- private func setupView() {
- sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
- }
-
- private func setupObserver() {
- NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
- }
+ open func sizeForItem(at indexPath: IndexPath) -> CGSize {
+ let calculator = cellSizeCalculatorForItem(at: indexPath)
+ return calculator.sizeForItem(at: indexPath)
+ }
- // MARK: - Attributes
-
- open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
- guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else {
- return nil
- }
- for attributes in attributesArray where attributes.representedElementCategory == .cell {
- let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
- cellSizeCalculator.configure(attributes: attributes)
- }
- return attributesArray
- }
+ /// Get all `MessageSizeCalculator`s
+ open func messageSizeCalculators() -> [MessageSizeCalculator] {
+ [
+ textMessageSizeCalculator,
+ attributedTextMessageSizeCalculator,
+ emojiMessageSizeCalculator,
+ photoMessageSizeCalculator,
+ videoMessageSizeCalculator,
+ locationMessageSizeCalculator,
+ audioMessageSizeCalculator,
+ contactMessageSizeCalculator,
+ linkPreviewMessageSizeCalculator,
+ ]
+ }
- open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
- guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else {
- return nil
- }
- if attributes.representedElementCategory == .cell {
- let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
- cellSizeCalculator.configure(attributes: attributes)
- }
- return attributes
- }
+ // MARK: Public
- // MARK: - Layout Invalidation
+ public private(set) var isTypingIndicatorViewHidden = true
- open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
- return collectionView?.bounds.width != newBounds.width
+ /// The `MessagesCollectionView` that owns this layout object.
+ public var messagesCollectionView: MessagesCollectionView {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.layoutUsedOnForeignType)
}
+ return messagesCollectionView
+ }
- open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
- let context = super.invalidationContext(forBoundsChange: newBounds)
- guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context }
- flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds)
- return flowLayoutContext
+ /// The `MessagesDataSource` for the layout's collection view.
+ public var messagesDataSource: MessagesDataSource {
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
+ return messagesDataSource
+ }
- @objc
- private func handleOrientationChange(_ notification: Notification) {
- invalidateLayout()
+ /// The `MessagesLayoutDelegate` for the layout's collection view.
+ public var messagesLayoutDelegate: MessagesLayoutDelegate {
+ guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
+ fatalError(MessageKitError.nilMessagesLayoutDelegate)
}
+ return messagesLayoutDelegate
+ }
- // MARK: - Cell Sizing
-
- lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
- lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
- lazy open var emojiMessageSizeCalculator: TextMessageSizeCalculator = {
- let sizeCalculator = TextMessageSizeCalculator(layout: self)
- sizeCalculator.messageLabelFont = UIFont.systemFont(ofSize: sizeCalculator.messageLabelFont.pointSize * 2)
- return sizeCalculator
- }()
- lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
- lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
- lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self)
-
- /// - Note:
- /// If you override this method, remember to call MessageLayoutDelegate's customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary
- open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
- switch message.kind {
- case .text:
- return textMessageSizeCalculator
- case .attributedText:
- return attributedTextMessageSizeCalculator
- case .emoji:
- return emojiMessageSizeCalculator
- case .photo:
- return photoMessageSizeCalculator
- case .video:
- return videoMessageSizeCalculator
- case .location:
- return locationMessageSizeCalculator
- case .custom:
- return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView)
- }
- }
+ public var itemWidth: CGFloat {
+ guard let collectionView = collectionView else { return 0 }
+ return collectionView.frame.width - sectionInset.left - sectionInset.right
+ }
- open func sizeForItem(at indexPath: IndexPath) -> CGSize {
- let calculator = cellSizeCalculatorForItem(at: indexPath)
- return calculator.sizeForItem(at: indexPath)
- }
-
- /// Set `incomingAvatarSize` of all `MessageSizeCalculator`s
- public func setMessageIncomingAvatarSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.incomingAvatarSize = newSize }
- }
-
- /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAvatarSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.outgoingAvatarSize = newSize }
- }
-
- /// Set `incomingAvatarPosition` of all `MessageSizeCalculator`s
- public func setMessageIncomingAvatarPosition(_ newPosition: AvatarPosition) {
- messageSizeCalculators().forEach { $0.incomingAvatarPosition = newPosition }
- }
-
- /// Set `outgoingAvatarPosition` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAvatarPosition(_ newPosition: AvatarPosition) {
- messageSizeCalculators().forEach { $0.outgoingAvatarPosition = newPosition }
- }
-
- /// Set `incomingMessagePadding` of all `MessageSizeCalculator`s
- public func setMessageIncomingMessagePadding(_ newPadding: UIEdgeInsets) {
- messageSizeCalculators().forEach { $0.incomingMessagePadding = newPadding }
- }
-
- /// Set `outgoingMessagePadding` of all `MessageSizeCalculator`s
- public func setMessageOutgoingMessagePadding(_ newPadding: UIEdgeInsets) {
- messageSizeCalculators().forEach { $0.outgoingMessagePadding = newPadding }
- }
-
- /// Set `incomingCellTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageIncomingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.incomingCellTopLabelAlignment = newAlignment }
- }
-
- /// Set `outgoingCellTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageOutgoingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.outgoingCellTopLabelAlignment = newAlignment }
- }
-
- /// Set `incomingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageIncomingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.incomingMessageTopLabelAlignment = newAlignment }
- }
-
- /// Set `outgoingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageOutgoingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.outgoingMessageTopLabelAlignment = newAlignment }
- }
-
- /// Set `incomingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageIncomingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.incomingMessageBottomLabelAlignment = newAlignment }
- }
-
- /// Set `outgoingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment }
- }
+ /// Set `incomingAvatarSize` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAvatarSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.incomingAvatarSize = newSize }
+ }
- /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s
- public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize }
- }
+ /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAvatarSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.outgoingAvatarSize = newSize }
+ }
- /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize }
- }
+ /// Set `incomingAvatarPosition` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAvatarPosition(_ newPosition: AvatarPosition) {
+ messageSizeCalculators().forEach { $0.incomingAvatarPosition = newPosition }
+ }
- /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s
- public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
- messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding }
- }
+ /// Set `outgoingAvatarPosition` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAvatarPosition(_ newPosition: AvatarPosition) {
+ messageSizeCalculators().forEach { $0.outgoingAvatarPosition = newPosition }
+ }
- /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
- messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding }
- }
+ /// Set `avatarLeadingTrailingPadding` of all `MessageSizeCalculator`s
+ public func setAvatarLeadingTrailingPadding(_ newPadding: CGFloat) {
+ messageSizeCalculators().forEach { $0.avatarLeadingTrailingPadding = newPadding }
+ }
- /// Get all `MessageSizeCalculator`s
- open func messageSizeCalculators() -> [MessageSizeCalculator] {
- return [textMessageSizeCalculator, attributedTextMessageSizeCalculator, emojiMessageSizeCalculator, photoMessageSizeCalculator, videoMessageSizeCalculator, locationMessageSizeCalculator]
- }
-
+ /// Set `incomingMessagePadding` of all `MessageSizeCalculator`s
+ public func setMessageIncomingMessagePadding(_ newPadding: UIEdgeInsets) {
+ messageSizeCalculators().forEach { $0.incomingMessagePadding = newPadding }
+ }
+
+ /// Set `outgoingMessagePadding` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingMessagePadding(_ newPadding: UIEdgeInsets) {
+ messageSizeCalculators().forEach { $0.outgoingMessagePadding = newPadding }
+ }
+
+ /// Set `incomingCellTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingCellTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingCellTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingCellTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingCellBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingCellBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingCellBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingCellBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingMessageTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingMessageTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingMessageBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize }
+ }
+
+ /// Set `outgoingAccessoryViewSize` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize }
+ }
+
+ /// Set `incomingAccessoryViewPadding` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
+ messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding }
+ }
+
+ /// Set `outgoingAccessoryViewPadding` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
+ messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding }
+ }
+
+ /// Set `incomingAccessoryViewPosition` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAccessoryViewPosition(_ newPosition: AccessoryPosition) {
+ messageSizeCalculators().forEach { $0.incomingAccessoryViewPosition = newPosition }
+ }
+
+ /// Set `outgoingAccessoryViewPosition` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAccessoryViewPosition(_ newPosition: AccessoryPosition) {
+ messageSizeCalculators().forEach { $0.outgoingAccessoryViewPosition = newPosition }
+ }
+
+ // MARK: Internal
+
+ // MARK: - Typing Indicator API
+
+ /// Notifies the layout that the typing indicator will change state
+ ///
+ /// - Parameters:
+ /// - isHidden: A Boolean value that is to be the new state of the typing indicator
+ internal func setTypingIndicatorViewHidden(_ isHidden: Bool) {
+ isTypingIndicatorViewHidden = isHidden
+ }
+
+ // MARK: Private
+
+ // MARK: - Methods
+
+ private func setupView() {
+ sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
+ }
+
+ private func setupObserver() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)),
+ name: UIDevice.orientationDidChangeNotification,
+ object: nil)
+ }
+
+ @objc
+ private func handleOrientationChange(_: Notification) {
+ invalidateLayout()
+ }
}
diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift
index e055ac570..ff8fefad6 100644
--- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift
+++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift
@@ -1,96 +1,91 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// The layout attributes used by a `MessageCollectionViewCell` to layout its subviews.
-open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
+open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
+ // MARK: Open
- // MARK: - Properties
+ // MARK: - Methods
- public var avatarSize: CGSize = .zero
- public var avatarPosition = AvatarPosition(vertical: .cellBottom)
+ open override func copy(with zone: NSZone? = nil) -> Any {
+ // swiftlint:disable force_cast
+ let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes
+ copy.avatarSize = avatarSize
+ copy.avatarPosition = avatarPosition
+ copy.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding
+ copy.messageContainerSize = messageContainerSize
+ copy.messageContainerPadding = messageContainerPadding
+ copy.messageLabelFont = messageLabelFont
+ copy.messageLabelInsets = messageLabelInsets
+ copy.cellTopLabelAlignment = cellTopLabelAlignment
+ copy.cellTopLabelSize = cellTopLabelSize
+ copy.cellBottomLabelAlignment = cellBottomLabelAlignment
+ copy.cellBottomLabelSize = cellBottomLabelSize
+ copy.messageTimeLabelSize = messageTimeLabelSize
+ copy.messageTopLabelAlignment = messageTopLabelAlignment
+ copy.messageTopLabelSize = messageTopLabelSize
+ copy.messageBottomLabelAlignment = messageBottomLabelAlignment
+ copy.messageBottomLabelSize = messageBottomLabelSize
+ copy.accessoryViewSize = accessoryViewSize
+ copy.accessoryViewPadding = accessoryViewPadding
+ copy.accessoryViewPosition = accessoryViewPosition
+ copy.linkPreviewFonts = linkPreviewFonts
+ return copy
+ // swiftlint:enable force_cast
+ }
- public var messageContainerSize: CGSize = .zero
- public var messageContainerPadding: UIEdgeInsets = .zero
- public var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
- public var messageLabelInsets: UIEdgeInsets = .zero
+ // MARK: - Properties
- public var cellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var cellTopLabelSize: CGSize = .zero
-
- public var messageTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var messageTopLabelSize: CGSize = .zero
+ public var avatarSize: CGSize = .zero
+ public var avatarPosition = AvatarPosition(vertical: .cellBottom)
+ public var avatarLeadingTrailingPadding: CGFloat = 0
- public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var messageBottomLabelSize: CGSize = .zero
+ public var messageContainerSize: CGSize = .zero
+ public var messageContainerPadding: UIEdgeInsets = .zero
+ public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
+ public var messageLabelInsets: UIEdgeInsets = .zero
- public var accessoryViewSize: CGSize = .zero
- public var accessoryViewPadding: HorizontalEdgeInsets = .zero
+ public var cellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var cellTopLabelSize: CGSize = .zero
- // MARK: - Methods
+ public var cellBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var cellBottomLabelSize: CGSize = .zero
- open override func copy(with zone: NSZone? = nil) -> Any {
- // swiftlint:disable force_cast
- let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes
- copy.avatarSize = avatarSize
- copy.avatarPosition = avatarPosition
- copy.messageContainerSize = messageContainerSize
- copy.messageContainerPadding = messageContainerPadding
- copy.messageLabelFont = messageLabelFont
- copy.messageLabelInsets = messageLabelInsets
- copy.cellTopLabelAlignment = cellTopLabelAlignment
- copy.cellTopLabelSize = cellTopLabelSize
- copy.messageTopLabelAlignment = messageTopLabelAlignment
- copy.messageTopLabelSize = messageTopLabelSize
- copy.messageBottomLabelAlignment = messageBottomLabelAlignment
- copy.messageBottomLabelSize = messageBottomLabelSize
- copy.accessoryViewSize = accessoryViewSize
- copy.accessoryViewPadding = accessoryViewPadding
- return copy
- // swiftlint:enable force_cast
- }
+ public var messageTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var messageTopLabelSize: CGSize = .zero
- open override func isEqual(_ object: Any?) -> Bool {
- // MARK: - LEAVE this as is
- if let attributes = object as? MessagesCollectionViewLayoutAttributes {
- return super.isEqual(object) && attributes.avatarSize == avatarSize
- && attributes.avatarPosition == attributes.avatarPosition
- && attributes.messageContainerSize == messageContainerSize
- && attributes.messageContainerPadding == messageContainerPadding
- && attributes.messageLabelFont == messageLabelFont
- && attributes.messageLabelInsets == messageLabelInsets
- && attributes.cellTopLabelAlignment == cellTopLabelAlignment
- && attributes.cellTopLabelSize == cellTopLabelSize
- && attributes.messageTopLabelAlignment == messageTopLabelAlignment
- && attributes.messageTopLabelSize == messageTopLabelSize
- && attributes.messageBottomLabelAlignment == messageBottomLabelAlignment
- && attributes.messageBottomLabelSize == messageBottomLabelSize
- && attributes.accessoryViewSize == accessoryViewSize
- && attributes.accessoryViewPadding == accessoryViewPadding
- } else {
- return false
- }
- }
+ public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var messageBottomLabelSize: CGSize = .zero
+
+ public var messageTimeLabelSize: CGSize = .zero
+
+ public var accessoryViewSize: CGSize = .zero
+ public var accessoryViewPadding: HorizontalEdgeInsets = .zero
+ public var accessoryViewPosition: AccessoryPosition = .messageCenter
+
+ public var linkPreviewFonts = LinkPreviewFonts(
+ titleFont: .preferredFont(forTextStyle: .footnote),
+ teaserFont: .preferredFont(forTextStyle: .caption2),
+ domainFont: .preferredFont(forTextStyle: .caption1))
}
diff --git a/Sources/Layout/TextMessageSizeCalculator.swift b/Sources/Layout/TextMessageSizeCalculator.swift
index 9f2b5cc16..85cb3890f 100644
--- a/Sources/Layout/TextMessageSizeCalculator.swift
+++ b/Sources/Layout/TextMessageSizeCalculator.swift
@@ -1,90 +1,95 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
open class TextMessageSizeCalculator: MessageSizeCalculator {
+ // MARK: Open
+
+ open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath)
+ let textInsets = messageLabelInsets(for: message)
+ return maxWidth - textInsets.horizontal
+ }
+
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+
+ var messageContainerSize: CGSize
+ let attributedText: NSAttributedString
+
+ let textMessageKind = message.kind.textMessageKind
+ switch textMessageKind {
+ case .attributedText(let text):
+ attributedText = text
+ case .text(let text), .emoji(let text):
+ attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont])
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
+ }
- public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14)
- public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18)
+ messageContainerSize = labelSize(for: attributedText, considering: maxWidth)
- public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
+ let messageInsets = messageLabelInsets(for: message)
+ messageContainerSize.width += messageInsets.horizontal
+ messageContainerSize.height += messageInsets.vertical
- internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets
- }
+ return messageContainerSize
+ }
- open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat {
- let maxWidth = super.messageContainerMaxWidth(for: message)
- let textInsets = messageLabelInsets(for: message)
- return maxWidth - textInsets.horizontal
- }
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ super.configure(attributes: attributes)
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- let maxWidth = messageContainerMaxWidth(for: message)
+ let dataSource = messagesLayout.messagesDataSource
+ let indexPath = attributes.indexPath
+ let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
- var messageContainerSize: CGSize
- let attributedText: NSAttributedString
+ attributes.messageLabelInsets = messageLabelInsets(for: message)
+ attributes.messageLabelFont = messageLabelFont
- switch message.kind {
- case .attributedText(let text):
- attributedText = text
- case .text(let text), .emoji(let text):
- attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont])
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ switch message.kind {
+ case .attributedText(let text):
+ guard !text.string.isEmpty else { return }
+ guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return }
+ attributes.messageLabelFont = font
+ default:
+ break
+ }
+ }
- messageContainerSize = labelSize(for: attributedText, considering: maxWidth)
+ // MARK: Public
- let messageInsets = messageLabelInsets(for: message)
- messageContainerSize.width += messageInsets.horizontal
- messageContainerSize.height += messageInsets.vertical
+ public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14)
+ public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18)
- return messageContainerSize
- }
+ public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
- open override func configure(attributes: UICollectionViewLayoutAttributes) {
- super.configure(attributes: attributes)
- guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
-
- let dataSource = messagesLayout.messagesDataSource
- let indexPath = attributes.indexPath
- let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
-
- attributes.messageLabelInsets = messageLabelInsets(for: message)
- attributes.messageLabelFont = messageLabelFont
-
- switch message.kind {
- case .attributedText(let text):
- guard !text.string.isEmpty else { return }
- guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return }
- attributes.messageLabelFont = font
- default:
- break
- }
- }
+ // MARK: Internal
+
+ internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets
+ }
}
diff --git a/Sources/Layout/TypingIndicatorCellSizeCalculator.swift b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift
new file mode 100644
index 000000000..f8d687956
--- /dev/null
+++ b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift
@@ -0,0 +1,39 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import UIKit
+
+open class TypingCellSizeCalculator: CellSizeCalculator {
+ // MARK: Lifecycle
+
+ public init(layout: MessagesCollectionViewFlowLayout? = nil) {
+ super.init()
+ self.layout = layout
+ }
+
+ // MARK: Open
+
+ open override func sizeForItem(at _: IndexPath) -> CGSize {
+ guard let layout = layout as? MessagesCollectionViewFlowLayout else { return .zero }
+ return layout.messagesLayoutDelegate.typingIndicatorViewSize(for: layout)
+ }
+}
diff --git a/Sources/Models/AccessoryPosition.swift b/Sources/Models/AccessoryPosition.swift
new file mode 100644
index 000000000..59c09c5ce
--- /dev/null
+++ b/Sources/Models/AccessoryPosition.swift
@@ -0,0 +1,45 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+
+/// Used to determine the `Horizontal` and `Vertical` position of
+/// an `AccessoryView` in a `MessageCollectionViewCell`.
+public enum AccessoryPosition: Equatable {
+ /// Aligns the `AccessoryView`'s top edge to the cell's top edge.
+ case cellTop
+
+ /// Aligns the `AccessoryView`'s top edge to the `messageTopLabel`'s top edge.
+ case messageLabelTop
+
+ /// Aligns the `AccessoryView`'s top edge to the `MessageContainerView`'s top edge.
+ case messageTop
+
+ /// Aligns the `AccessoryView` center to the `MessageContainerView` center.
+ case messageCenter
+
+ /// Aligns the `AccessoryView`'s bottom edge to the `MessageContainerView`s bottom edge.
+ case messageBottom
+
+ /// Aligns the `AccessoryView`'s bottom edge to the cell's bottom edge.
+ case cellBottom
+}
diff --git a/Sources/Models/Avatar.swift b/Sources/Models/Avatar.swift
index 29fa16943..42872a755 100644
--- a/Sources/Models/Avatar.swift
+++ b/Sources/Models/Avatar.swift
@@ -1,47 +1,44 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017-2018 MessageKit
+import Foundation
+import UIKit
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+/// An object used to group the information to be used by an `AvatarView`.
+public struct Avatar {
+ // MARK: - Properties
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
+ /// The image to be used for an `AvatarView`.
+ public let image: UIImage?
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+ /// The placeholder initials to be used in the case where no image is provided.
+ ///
+ /// The default value of this property is "?".
+ public var initials = "?"
-import Foundation
+ // MARK: - Initializer
-/// An object used to group the information to be used by an `AvatarView`.
-public struct Avatar {
-
- // MARK: - Properties
-
- /// The image to be used for an `AvatarView`.
- public let image: UIImage?
-
- /// The placeholder initials to be used in the case where no image is provided.
- ///
- /// The default value of this property is "?".
- public var initials: String = "?"
-
- // MARK: - Initializer
-
- public init(image: UIImage? = nil, initials: String = "?") {
- self.image = image
- self.initials = initials
- }
-
+ public init(image: UIImage? = nil, initials: String = "?") {
+ self.image = image
+ self.initials = initials
+ }
}
diff --git a/Sources/Models/AvatarPosition.swift b/Sources/Models/AvatarPosition.swift
index 3cf2bca25..9f6000a26 100644
--- a/Sources/Models/AvatarPosition.swift
+++ b/Sources/Models/AvatarPosition.swift
@@ -1,98 +1,92 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
+
+// MARK: - AvatarPosition
/// Used to determine the `Horizontal` and `Vertical` position of
// an `AvatarView` in a `MessageCollectionViewCell`.
-public struct AvatarPosition {
-
- /// An enum representing the horizontal alignment of an `AvatarView`.
- public enum Horizontal {
-
- /// Positions the `AvatarView` on the side closest to the cell's leading edge.
- case cellLeading
-
- /// Positions the `AvatarView` on the side closest to the cell's trailing edge.
- case cellTrailing
-
- /// Positions the `AvatarView` based on whether the message is from the current Sender.
- /// The cell is positioned `.cellTrailling` if `isFromCurrentSender` is true
- /// and `.cellLeading` if false.
- case natural
- }
-
- /// An enum representing the verical alignment for an `AvatarView`.
- public enum Vertical {
-
- /// Aligns the `AvatarView`'s top edge to the cell's top edge.
- case cellTop
-
- /// Aligns the `AvatarView`'s top edge to the `messageTopLabel`'s top edge.
- case messageLabelTop
-
- /// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge.
- case messageTop
-
- /// Aligns the `AvatarView` center to the `MessageContainerView` center.
- case messageCenter
-
- /// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge.
- case messageBottom
-
- /// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge.
- case cellBottom
-
- }
-
- // MARK: - Properties
-
- // The vertical position
- public var vertical: Vertical
-
- // The horizontal position
- public var horizontal: Horizontal
-
- // MARK: - Initializers
-
- public init(horizontal: Horizontal, vertical: Vertical) {
- self.horizontal = horizontal
- self.vertical = vertical
- }
-
- public init(vertical: Vertical) {
- self.init(horizontal: .natural, vertical: vertical)
- }
-
-}
+public struct AvatarPosition: Equatable {
+ // MARK: Lifecycle
-// MARK: - Equatable Conformance
+ public init(horizontal: Horizontal, vertical: Vertical) {
+ self.horizontal = horizontal
+ self.vertical = vertical
+ }
+
+ public init(vertical: Vertical) {
+ self.init(horizontal: .natural, vertical: vertical)
+ }
+
+ // MARK: Public
+
+ /// An enum representing the horizontal alignment of an `AvatarView`.
+ public enum Horizontal {
+ /// Positions the `AvatarView` on the side closest to the cell's leading edge.
+ case cellLeading
+
+ /// Positions the `AvatarView` on the side closest to the cell's trailing edge.
+ case cellTrailing
+
+ /// Positions the `AvatarView` based on whether the message is from the current Sender.
+ /// The cell is positioned `.cellTrailing` if `isFromCurrentSender` is true
+ /// and `.cellLeading` if false.
+ case natural
+ }
+
+ /// An enum representing the vertical alignment for an `AvatarView`.
+ public enum Vertical {
+ /// Aligns the `AvatarView`'s top edge to the cell's top edge.
+ case cellTop
-extension AvatarPosition: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl
+ /// Aligns the `AvatarView`'s top edge to the `messageTopLabel`'s top edge.
+ case messageLabelTop
- public static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool {
- return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal
- }
+ /// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge.
+ case messageTop
+
+ /// Aligns the `AvatarView` center to the `MessageContainerView` center.
+ case messageCenter
+
+ /// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge.
+ case messageBottom
+
+ /// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge.
+ case cellBottom
+ }
+
+ // The vertical position
+ public var vertical: Vertical
+
+ // The horizontal position
+ public var horizontal: Horizontal
+}
+
+// MARK: - Equatable Conformance
+extension AvatarPosition {
+ public static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool {
+ lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal
+ }
}
diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift
index a49b7b6d9..7fd0cb228 100644
--- a/Sources/Models/DetectorType.swift
+++ b/Sources/Models/DetectorType.swift
@@ -1,51 +1,80 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-public enum DetectorType {
+public enum DetectorType: Hashable, Sendable {
+ case address
+ case date
+ case phoneNumber
+ case url
+ case transitInformation
+ case custom(NSRegularExpression)
- case address
- case date
- case phoneNumber
- case url
- case transitInformation
+ // MARK: Public
- // MARK: - Not supported yet
+ // swiftlint:disable force_try
+ public static let hashtag = DetectorType.custom(try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: []))
+ public static let mention = DetectorType.custom(try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: []))
- //case mention
- //case hashtag
- //case custom
+ /// Simply check if the detector type is a .custom
+ public var isCustom: Bool {
+ switch self {
+ case .custom: return true
+ default: return false
+ }
+ }
+
+ /// The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted.
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(toInt())
+ }
+
+ // MARK: Internal
- internal var textCheckingType: NSTextCheckingResult.CheckingType {
- switch self {
- case .address: return .address
- case .date: return .date
- case .phoneNumber: return .phoneNumber
- case .url: return .link
- case .transitInformation: return .transitInformation
- }
+ // swiftlint:enable force_try
+
+ internal var textCheckingType: NSTextCheckingResult.CheckingType {
+ switch self {
+ case .address: return .address
+ case .date: return .date
+ case .phoneNumber: return .phoneNumber
+ case .url: return .link
+ case .transitInformation: return .transitInformation
+ case .custom: return .regularExpression
}
+ }
+
+ // MARK: Private
+ /// Return an 'Int' value for each `DetectorType` type so `DetectorType` can conform to `Hashable`
+ private func toInt() -> Int {
+ switch self {
+ case .address: return 0
+ case .date: return 1
+ case .phoneNumber: return 2
+ case .url: return 3
+ case .transitInformation: return 4
+ case .custom(let regex): return regex.hashValue
+ }
+ }
}
diff --git a/Sources/Models/HorizontalEdgeInsets.swift b/Sources/Models/HorizontalEdgeInsets.swift
index 6f42cffbc..86cfc3f2d 100644
--- a/Sources/Models/HorizontalEdgeInsets.swift
+++ b/Sources/Models/HorizontalEdgeInsets.swift
@@ -1,55 +1,55 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
-/// A varient of `UIEdgeInsets` that only has horizontal inset properties
-public struct HorizontalEdgeInsets {
+// MARK: - HorizontalEdgeInsets
- public var left: CGFloat
- public var right: CGFloat
+/// A variant of `UIEdgeInsets` that only has horizontal inset properties
+public struct HorizontalEdgeInsets: Equatable {
+ public var left: CGFloat
+ public var right: CGFloat
- public init(left: CGFloat, right: CGFloat) {
- self.left = left
- self.right = right
- }
+ public init(left: CGFloat, right: CGFloat) {
+ self.left = left
+ self.right = right
+ }
- public static var zero: HorizontalEdgeInsets {
- return HorizontalEdgeInsets(left: 0, right: 0)
- }
+ public static var zero: HorizontalEdgeInsets {
+ HorizontalEdgeInsets(left: 0, right: 0)
+ }
}
-extension HorizontalEdgeInsets: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl
+// MARK: Equatable Conformance
- public static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool {
- return lhs.left == rhs.left && lhs.right == rhs.right
- }
+extension HorizontalEdgeInsets {
+ public static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool {
+ lhs.left == rhs.left && lhs.right == rhs.right
+ }
}
-internal extension HorizontalEdgeInsets {
-
- var horizontal: CGFloat { // swiftlint:disable:this explicit_acl
- return left + right
- }
+extension HorizontalEdgeInsets {
+ internal var horizontal: CGFloat {
+ left + right
+ }
}
diff --git a/Sources/Models/LabelAlignment.swift b/Sources/Models/LabelAlignment.swift
index 29624455c..fc9616dcf 100644
--- a/Sources/Models/LabelAlignment.swift
+++ b/Sources/Models/LabelAlignment.swift
@@ -1,47 +1,43 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
-public struct LabelAlignment {
+// MARK: - LabelAlignment
- public var textAlignment: NSTextAlignment
- public var textInsets: UIEdgeInsets
-
- public init(textAlignment: NSTextAlignment, textInsets: UIEdgeInsets) {
- self.textAlignment = textAlignment
- self.textInsets = textInsets
- }
+public struct LabelAlignment: Equatable {
+ public var textAlignment: NSTextAlignment
+ public var textInsets: UIEdgeInsets
+ public init(textAlignment: NSTextAlignment, textInsets: UIEdgeInsets) {
+ self.textAlignment = textAlignment
+ self.textInsets = textInsets
+ }
}
// MARK: - Equatable Conformance
-extension LabelAlignment: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl
-
- public static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool {
- return lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets
- }
-
+extension LabelAlignment {
+ public static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool {
+ lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets
+ }
}
diff --git a/Sources/Models/LinkPreviewFonts.swift b/Sources/Models/LinkPreviewFonts.swift
new file mode 100644
index 000000000..8d6c14733
--- /dev/null
+++ b/Sources/Models/LinkPreviewFonts.swift
@@ -0,0 +1,30 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+public struct LinkPreviewFonts: Equatable {
+ let titleFont: UIFont
+ let teaserFont: UIFont
+ let domainFont: UIFont
+}
diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift
index 21628fcb0..792a07310 100644
--- a/Sources/Models/LocationMessageSnapshotOptions.swift
+++ b/Sources/Models/LocationMessageSnapshotOptions.swift
@@ -1,64 +1,70 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017-2018 MessageKit
+import MapKit
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+/// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`.
+@MainActor
+public struct LocationMessageSnapshotOptions {
+ // MARK: Lifecycle
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
+ /// Initialize LocationMessageSnapshotOptions with given parameters
+ ///
+ /// - Parameters:
+ /// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings.
+ /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest.
+ /// - span: The span of the snapshot.
+ /// - scale: The scale of the snapshot.
+ public init(
+ showsBuildings: Bool = false,
+ showsPointsOfInterest: Bool = false,
+ span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0),
+ scale: CGFloat = UIScreen.main.scale)
+ {
+ self.showsBuildings = showsBuildings
+ self.showsPointsOfInterest = showsPointsOfInterest
+ self.span = span
+ self.scale = scale
+ }
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+ // MARK: Public
-import MapKit
+ /// A Boolean value indicating whether the snapshot image should display buildings.
+ ///
+ /// The default value of this property is `false`.
+ public var showsBuildings: Bool
-/// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`.
-public struct LocationMessageSnapshotOptions {
+ /// A Boolean value indicating whether the snapshot image should display points of interest.
+ ///
+ /// The default value of this property is `false`.
+ public var showsPointsOfInterest: Bool
+
+ /// The span of the snapshot.
+ ///
+ /// The default value of this property uses a width of `0` and height of `0`.
+ public var span: MKCoordinateSpan
- /// Initialize LocationMessageSnapshotOptions with given parameters
- ///
- /// - Parameters:
- /// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings.
- /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest.
- /// - span: The span of the snapshot.
- /// - scale: The scale of the snapshot.
- public init(showsBuildings: Bool = false, showsPointsOfInterest: Bool = false, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0), scale: CGFloat = UIScreen.main.scale) {
- self.showsBuildings = showsBuildings
- self.showsPointsOfInterest = showsPointsOfInterest
- self.span = span
- self.scale = scale
- }
-
- /// A Boolean value indicating whether the snapshot image should display buildings.
- ///
- /// The default value of this property is `false`.
- public var showsBuildings: Bool
-
- /// A Boolean value indicating whether the snapshot image should display points of interest.
- ///
- /// The default value of this property is `false`.
- public var showsPointsOfInterest: Bool
-
- /// The span of the snapshot.
- ///
- /// The default value of this property uses a width of `0` and height of `0`.
- public var span: MKCoordinateSpan
-
- /// The scale of the snapshot.
- ///
- /// The default value of this property uses the `UIScreen.main.scale`.
- public var scale: CGFloat
-
+ /// The scale of the snapshot.
+ ///
+ /// The default value of this property uses the `UIScreen.main.scale`.
+ public var scale: CGFloat
}
diff --git a/Sources/Models/MessageInputBarKind.swift b/Sources/Models/MessageInputBarKind.swift
new file mode 100644
index 000000000..6a127b01c
--- /dev/null
+++ b/Sources/Models/MessageInputBarKind.swift
@@ -0,0 +1,29 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+public enum MessageInputBarKind: Equatable {
+ case messageInputBar
+ case custom(UIView)
+}
diff --git a/Sources/Models/MessageKind.swift b/Sources/Models/MessageKind.swift
index a29d76d5c..cb11f2d12 100644
--- a/Sources/Models/MessageKind.swift
+++ b/Sources/Models/MessageKind.swift
@@ -1,68 +1,71 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
/// An enum representing the kind of message and its underlying kind.
public enum MessageKind {
+ /// A standard text message.
+ ///
+ /// - Note: The font used for this message will be the value of the
+ /// `messageLabelFont` property in the `MessagesCollectionViewFlowLayout` object.
+ ///
+ /// Using `MessageKind.attributedText(NSAttributedString)` doesn't require you
+ /// to set this property and results in higher performance.
+ case text(String)
- /// A standard text message.
- ///
- /// - Note: The font used for this message will be the value of the
- /// `messageLabelFont` property in the `MessagesCollectionViewFlowLayout` object.
- ///
- /// Using `MessageKind.attributedText(NSAttributedString)` doesn't require you
- /// to set this property and results in higher performance.
- case text(String)
-
- /// A message with attributed text.
- case attributedText(NSAttributedString)
+ /// A message with attributed text.
+ case attributedText(NSAttributedString)
- /// A photo message.
- case photo(MediaItem)
+ /// A photo message.
+ case photo(MediaItem)
- /// A video message.
- case video(MediaItem)
+ /// A video message.
+ case video(MediaItem)
- /// A location message.
- case location(LocationItem)
+ /// A location message.
+ case location(LocationItem)
- /// An emoji message.
- case emoji(String)
+ /// An emoji message.
+ case emoji(String)
- /// A custom message.
- /// - Note: Using this case requires that you implement the following methods and handle this case:
- /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
- /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
- case custom(Any?)
+ /// An audio message.
+ case audio(AudioItem)
- // MARK: - Not supported yet
+ /// A contact message.
+ case contact(ContactItem)
+
+ /// A link preview message.
+ case linkPreview(LinkItem)
+
+ /// A custom message.
+ /// - Note: Using this case requires that you implement the following methods and handle this case:
+ /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
+ /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
+ case custom(Any?)
+
+ // MARK: - Not supported yet
-// case audio(Data)
-//
// case system(String)
-//
+//
// case placeholder
-
}
diff --git a/Sources/Models/MessageKitDateFormatter.swift b/Sources/Models/MessageKitDateFormatter.swift
index c0d02bc27..c3636034e 100644
--- a/Sources/Models/MessageKitDateFormatter.swift
+++ b/Sources/Models/MessageKitDateFormatter.swift
@@ -1,66 +1,70 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017-2018 MessageKit
+import Foundation
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+open class MessageKitDateFormatter: @unchecked Sendable {
+ // MARK: Lifecycle
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
+ // MARK: - Initializer
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+ private init() { }
-import Foundation
+ // MARK: Open
-open class MessageKitDateFormatter {
+ open func configureDateFormatter(for date: Date) {
+ switch true {
+ case Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date):
+ formatter.doesRelativeDateFormatting = true
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+ case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .weekOfYear):
+ formatter.dateFormat = "EEEE h:mm a"
+ case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .year):
+ formatter.dateFormat = "E, d MMM, h:mm a"
+ default:
+ formatter.dateFormat = "MMM d, yyyy, h:mm a"
+ }
+ }
- // MARK: - Properties
+ // MARK: Public
- public static let shared = MessageKitDateFormatter()
+ // MARK: - Properties
- private let formatter = DateFormatter()
+ public static let shared = MessageKitDateFormatter()
- // MARK: - Initializer
+ // MARK: - Methods
- private init() {}
+ public func string(from date: Date) -> String {
+ configureDateFormatter(for: date)
+ return formatter.string(from: date)
+ }
- // MARK: - Methods
+ public func attributedString(from date: Date, with attributes: [NSAttributedString.Key: Any]) -> NSAttributedString {
+ let dateString = string(from: date)
+ return NSAttributedString(string: dateString, attributes: attributes)
+ }
- public func string(from date: Date) -> String {
- configureDateFormatter(for: date)
- return formatter.string(from: date)
- }
+ // MARK: Private
- public func attributedString(from date: Date, with attributes: [NSAttributedString.Key: Any]) -> NSAttributedString {
- let dateString = string(from: date)
- return NSAttributedString(string: dateString, attributes: attributes)
- }
-
- open func configureDateFormatter(for date: Date) {
- switch true {
- case Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date):
- formatter.doesRelativeDateFormatting = true
- formatter.dateStyle = .short
- formatter.timeStyle = .short
- case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .weekOfYear):
- formatter.dateFormat = "EEEE h:mm a"
- case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .year):
- formatter.dateFormat = "E, d MMM, h:mm a"
- default:
- formatter.dateFormat = "MMM d, yyyy, h:mm a"
- }
- }
-
+ private let formatter = DateFormatter()
}
diff --git a/Sources/Models/MessageKitError.swift b/Sources/Models/MessageKitError.swift
index c30ac5709..2ac4ca46b 100644
--- a/Sources/Models/MessageKitError.swift
+++ b/Sources/Models/MessageKitError.swift
@@ -1,38 +1,36 @@
-/*
- MIT License
-
- Copyright (c) 2017 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
internal enum MessageKitError {
- internal static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved."
- internal static let nilMessagesDataSource = "MessagesDataSource has not been set."
- internal static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set."
- internal static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set."
- internal static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView."
- internal static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type."
- internal static let unrecognizedSectionKind = "Received unrecognized element kind:"
- internal static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType"
- internal static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle"
- internal static let couldNotCreateAssetsPath = "MessageKit: Could not create path to the assets bundle."
- internal static let customDataUnresolvedCell = "Did not return a cell for MessageKind.custom(Any)."
- internal static let customDataUnresolvedSize = "Did not return a size for MessageKind.custom(Any)."
+ static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved."
+ static let nilMessagesDataSource = "MessagesDataSource has not been set."
+ static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set."
+ static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set."
+ static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView."
+ static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type."
+ static let unrecognizedSectionKind = "Received unrecognized element kind:"
+ static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType"
+ static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle"
+ static let customDataUnresolvedCell = "Did not return a cell for MessageKind.custom(Any)."
+ static let customDataUnresolvedSize = "Did not return a size for MessageKind.custom(Any)."
+ static let couldNotFindColorAsset = "MessageKit: Could not load the color asset."
}
diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift
index f8abd50e9..ed1dbeba4 100644
--- a/Sources/Models/MessageStyle.swift
+++ b/Sources/Models/MessageStyle.swift
@@ -1,151 +1,157 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
public enum MessageStyle {
+ // MARK: - MessageStyle
- // MARK: - TailCorner
+ case none
+ case bubble
+ case bubbleOutline(UIColor)
+ case bubbleTail(TailCorner, TailStyle)
+ case bubbleTailOutline(UIColor, TailCorner, TailStyle)
+ case customImageTail(UIImage,TailCorner)
+ case custom((MessageContainerView) -> Void)
- public enum TailCorner: String {
+ // MARK: Public
- case topLeft
- case bottomLeft
- case topRight
- case bottomRight
+ // MARK: - TailCorner
- internal var imageOrientation: UIImage.Orientation {
- switch self {
+ public enum TailCorner: String {
+ case topLeft
+ case bottomLeft
+ case topRight
+ case bottomRight
+
+ internal var imageOrientation: UIImage.Orientation {
+ switch self {
case .bottomRight: return .up
case .bottomLeft: return .upMirrored
case .topLeft: return .down
case .topRight: return .downMirrored
- }
}
}
+ }
- // MARK: - TailStyle
-
- public enum TailStyle {
+ // MARK: - TailStyle
- case curved
- case pointedEdge
+ public enum TailStyle {
+ case curved
+ case pointedEdge
- internal var imageNameSuffix: String {
- switch self {
- case .curved:
- return "_tail_v2"
- case .pointedEdge:
- return "_tail_v1"
- }
- }
+ internal var imageNameSuffix: String {
+ switch self {
+ case .curved:
+ return "_tail_v2"
+ case .pointedEdge:
+ return "_tail_v1"
+ }
+ }
+ }
+
+ public var image: UIImage? {
+ if
+ let imageCacheKey = imageCacheKey,
+ let cachedImage = MessageStyle.bubbleImageCache.object(forKey: imageCacheKey as NSString)
+ {
+ return cachedImage
+ }
+
+ func strechAndCache(image: UIImage) -> UIImage {
+ let stretchedImage = stretch(image)
+ if let imageCacheKey = imageCacheKey {
+ MessageStyle.bubbleImageCache.setObject(stretchedImage, forKey: imageCacheKey as NSString)
+ }
+ return stretchedImage
+ }
+
+ if case .customImageTail(let image, let corner) = self {
+ guard let cgImage = image.cgImage else { return nil }
+ let image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation)
+ return strechAndCache(image: image)
}
- // MARK: - MessageStyle
-
- case none
- case bubble
- case bubbleOutline(UIColor)
- case bubbleTail(TailCorner, TailStyle)
- case bubbleTailOutline(UIColor, TailCorner, TailStyle)
- case custom((MessageContainerView) -> Void)
-
- // MARK: - Public
+ guard
+ let imageName = imageName,
+ var image = UIImage(named: imageName, in: Bundle.messageKitAssetBundle, compatibleWith: nil)
+ else {
+ return nil
+ }
- public var image: UIImage? {
-
- guard let imageCacheKey = imageCacheKey, let path = imagePath else { return nil }
+ switch self {
+ case .none, .custom:
+ return nil
+ case .bubble, .bubbleOutline, .customImageTail:
+ break
+ case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _):
+ guard let cgImage = image.cgImage else { return nil }
+ image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation)
+ }
- let cache = MessageStyle.bubbleImageCache
+ return strechAndCache(image: image)
+ }
- if let cachedImage = cache.object(forKey: imageCacheKey as NSString) {
- return cachedImage
- }
- guard var image = UIImage(contentsOfFile: path) else { return nil }
-
- switch self {
- case .none, .custom:
- return nil
- case .bubble, .bubbleOutline:
- break
- case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _):
- guard let cgImage = image.cgImage else { return nil }
- image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation)
- }
-
- let stretchedImage = stretch(image)
- cache.setObject(stretchedImage, forKey: imageCacheKey as NSString)
- return stretchedImage
- }
+ // MARK: Internal
- // MARK: - Internal
-
- internal static let bubbleImageCache: NSCache = {
+ nonisolated(unsafe) internal static let bubbleImageCache: NSCache = {
let cache = NSCache()
cache.name = "com.messagekit.MessageKit.bubbleImageCache"
return cache
}()
-
- // MARK: - Private
-
- private var imageCacheKey: String? {
- guard let imageName = imageName else { return nil }
-
- switch self {
- case .bubble, .bubbleOutline:
- return imageName
- case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _):
- return imageName + "_" + corner.rawValue
- default:
- return nil
- }
- }
- private var imageName: String? {
- switch self {
- case .bubble:
- return "bubble_full"
- case .bubbleOutline:
- return "bubble_outlined"
- case .bubbleTail(_, let tailStyle):
- return "bubble_full" + tailStyle.imageNameSuffix
- case .bubbleTailOutline(_, _, let tailStyle):
- return "bubble_outlined" + tailStyle.imageNameSuffix
- case .none, .custom:
- return nil
- }
- }
+ // MARK: Private
- private var imagePath: String? {
- guard let imageName = imageName else { return nil }
- let assetBundle = Bundle.messageKitAssetBundle()
- return assetBundle.path(forResource: imageName, ofType: "png", inDirectory: "Images")
- }
+ private var imageCacheKey: String? {
+ guard let imageName = imageName else { return nil }
- private func stretch(_ image: UIImage) -> UIImage {
- let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2)
- let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x)
- return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
+ switch self {
+ case .bubble, .bubbleOutline:
+ return imageName
+ case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _):
+ return imageName + "_" + corner.rawValue
+ default:
+ return nil
+ }
+ }
+
+ private var imageName: String? {
+ switch self {
+ case .bubble:
+ return "bubble_full"
+ case .bubbleOutline:
+ return "bubble_outlined"
+ case .bubbleTail(_, let tailStyle):
+ return "bubble_full" + tailStyle.imageNameSuffix
+ case .bubbleTailOutline(_, _, let tailStyle):
+ return "bubble_outlined" + tailStyle.imageNameSuffix
+ case .none, .custom, .customImageTail:
+ return nil
}
+ }
+
+ private func stretch(_ image: UIImage) -> UIImage {
+ let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2)
+ let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x)
+ return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
+ }
}
diff --git a/Sources/Models/NSConstraintLayoutSet.swift b/Sources/Models/NSConstraintLayoutSet.swift
deleted file mode 100644
index cd063dfdc..000000000
--- a/Sources/Models/NSConstraintLayoutSet.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import UIKit
-
-internal class NSLayoutConstraintSet {
-
- internal var top: NSLayoutConstraint?
- internal var bottom: NSLayoutConstraint?
- internal var left: NSLayoutConstraint?
- internal var right: NSLayoutConstraint?
- internal var centerX: NSLayoutConstraint?
- internal var centerY: NSLayoutConstraint?
- internal var width: NSLayoutConstraint?
- internal var height: NSLayoutConstraint?
-
- internal init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil,
- left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil,
- centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil,
- width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) {
- self.top = top
- self.bottom = bottom
- self.left = left
- self.right = right
- self.centerX = centerX
- self.centerY = centerY
- self.width = width
- self.height = height
- }
-
- /// All of the currently configured constraints
- private var availableConstraints: [NSLayoutConstraint] {
- let constraints = [top, bottom, left, right, centerX, centerY, width, height]
- var available: [NSLayoutConstraint] = []
- for constraint in constraints {
- if let value = constraint {
- available.append(value)
- }
- }
- return available
- }
-
- /// Activates all of the non-nil constraints
- ///
- /// - Returns: Self
- @discardableResult
- internal func activate() -> Self {
- NSLayoutConstraint.activate(availableConstraints)
- return self
- }
-
- /// Deactivates all of the non-nil constraints
- ///
- /// - Returns: Self
- @discardableResult
- internal func deactivate() -> Self {
- NSLayoutConstraint.deactivate(availableConstraints)
- return self
- }
-}
diff --git a/Sources/Models/Sender.swift b/Sources/Models/Sender.swift
deleted file mode 100644
index 41995d371..000000000
--- a/Sources/Models/Sender.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import Foundation
-
-/// An object that groups the metadata of a messages sender.
-public struct Sender {
-
- /// MARK: - Properties
-
- /// The unique String identifier for the sender.
- ///
- /// Note: This value must be unique across all senders.
- public let id: String
-
- /// The display name of a sender.
- public let displayName: String
-
- // MARK: - Intializers
-
- public init(id: String, displayName: String) {
- self.id = id
- self.displayName = displayName
- }
-}
-
-// MARK: - Equatable Conformance
-
-extension Sender: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl
-
- /// Two senders are considered equal if they have the same id.
- public static func == (left: Sender, right: Sender) -> Bool {
- return left.id == right.id
- }
-
-}
diff --git a/Sources/Protocols/AudioItem.swift b/Sources/Protocols/AudioItem.swift
new file mode 100644
index 000000000..db24e7c58
--- /dev/null
+++ b/Sources/Protocols/AudioItem.swift
@@ -0,0 +1,37 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import class AVFoundation.AVAudioPlayer
+import Foundation
+import UIKit
+
+/// A protocol used to represent the data for an audio message.
+public protocol AudioItem {
+ /// The url where the audio file is located.
+ var url: URL { get }
+
+ /// The audio file duration in seconds.
+ var duration: Float { get }
+
+ /// The size of the audio item.
+ var size: CGSize { get }
+}
diff --git a/Sources/Protocols/ContactItem.swift b/Sources/Protocols/ContactItem.swift
new file mode 100644
index 000000000..9a40fc78d
--- /dev/null
+++ b/Sources/Protocols/ContactItem.swift
@@ -0,0 +1,38 @@
+// MIT License
+//
+// Copyright (c) 2017-2018 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+
+/// A protocol used to represent the data for a contact message.
+public protocol ContactItem {
+ /// contact displayed name
+ var displayName: String { get }
+
+ /// initials from contact first and last name
+ var initials: String { get }
+
+ /// contact phone numbers
+ var phoneNumbers: [String] { get }
+
+ /// contact emails
+ var emails: [String] { get }
+}
diff --git a/Sources/Protocols/LinkItem.swift b/Sources/Protocols/LinkItem.swift
new file mode 100644
index 000000000..5d3342a6f
--- /dev/null
+++ b/Sources/Protocols/LinkItem.swift
@@ -0,0 +1,66 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import CoreGraphics
+import Foundation
+import UIKit
+
+// MARK: - LinkItem
+
+/// A protocol used to represent the data for a link preview message.
+public protocol LinkItem {
+ /// A link item needs a message to present, it can be a simple String or
+ /// a NSAttributedString, but only one will be shown.
+ /// LinkItem.text has priority over LinkItem.attributedText.
+
+ /// The message text.
+ var text: String? { get }
+
+ /// The message attributed text.
+ var attributedText: NSAttributedString? { get }
+
+ /// The URL.
+ var url: URL { get }
+
+ /// The title.
+ var title: String? { get }
+
+ /// The teaser text.
+ var teaser: String { get }
+
+ /// The thumbnail image.
+ var thumbnailImage: UIImage { get }
+}
+
+extension LinkItem {
+ public var textKind: MessageKind {
+ let kind: MessageKind
+ if let text = text {
+ kind = .text(text)
+ } else if let attributedText = attributedText {
+ kind = .attributedText(attributedText)
+ } else {
+ fatalError("LinkItem must have \"text\" or \"attributedText\"")
+ }
+ return kind
+ }
+}
diff --git a/Sources/Protocols/LocationItem.swift b/Sources/Protocols/LocationItem.swift
index f08fccb37..8789bfa8a 100644
--- a/Sources/Protocols/LocationItem.swift
+++ b/Sources/Protocols/LocationItem.swift
@@ -1,36 +1,34 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import class CoreLocation.CLLocation
+import Foundation
+import UIKit
/// A protocol used to represent the data for a location message.
public protocol LocationItem {
+ /// The location.
+ var location: CLLocation { get }
- /// The location.
- var location: CLLocation { get }
-
- /// The size of the location item.
- var size: CGSize { get }
-
+ /// The size of the location item.
+ var size: CGSize { get }
}
diff --git a/Sources/Protocols/MediaItem.swift b/Sources/Protocols/MediaItem.swift
index cddb94c7f..28160d4b8 100644
--- a/Sources/Protocols/MediaItem.swift
+++ b/Sources/Protocols/MediaItem.swift
@@ -1,42 +1,39 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
/// A protocol used to represent the data for a media message.
public protocol MediaItem {
+ /// The url where the media is located.
+ var url: URL? { get }
- /// The url where the media is located.
- var url: URL? { get }
-
- /// The image.
- var image: UIImage? { get }
-
- /// A placeholder image for when the image is obtained asychronously.
- var placeholderImage: UIImage { get }
+ /// The image.
+ var image: UIImage? { get }
- /// The size of the media item.
- var size: CGSize { get }
+ /// A placeholder image for when the image is obtained asynchronously.
+ var placeholderImage: UIImage { get }
+ /// The size of the media item.
+ var size: CGSize { get }
}
diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift
index 4fbe9da92..a243a773d 100644
--- a/Sources/Protocols/MessageCellDelegate.swift
+++ b/Sources/Protocols/MessageCellDelegate.swift
@@ -1,106 +1,188 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+// MARK: - MessageCellDelegate
+
/// A protocol used by `MessageContentCell` subclasses to detect taps in the cell's subviews.
public protocol MessageCellDelegate: MessageLabelDelegate {
+ /// Triggered when a tap occurs in the background of the cell.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap occurred.
+ ///
+ /// - Note:
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapBackground(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the `MessageContainerView`.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap occurred.
+ ///
+ /// - Note:
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapMessage(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the `AvatarView`.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapAvatar(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the cellTopLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell tap the touch occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapCellTopLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the cellBottomLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell tap the touch occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapCellBottomLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the messageTopLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell tap the touch occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapMessageTopLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the messageBottomLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapMessageBottomLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the accessoryView.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapAccessoryView(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs on the image.
+ ///
+ /// - Parameters:
+ /// - cell: The image where the touch occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapImage(in cell: MessageCollectionViewCell)
- /// Triggered when a tap occurs in the `MessageContainerView`.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap occurred.
- ///
- /// - Note:
- /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
- /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
- /// method `messageForItem(at:indexPath:messagesCollectionView)`.
- func didTapMessage(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the `AvatarView`.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap occurred.
- ///
- /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
- /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
- /// method `messageForItem(at:indexPath:messagesCollectionView)`.
- func didTapAvatar(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the cellTopLabel.
- ///
- /// - Parameters:
- /// - cell: The cell tap the touch occurred.
- ///
- /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
- /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
- /// method `messageForItem(at:indexPath:messagesCollectionView)`.
- func didTapCellTopLabel(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the messageTopLabel.
- ///
- /// - Parameters:
- /// - cell: The cell tap the touch occurred.
- ///
- /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
- /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
- /// method `messageForItem(at:indexPath:messagesCollectionView)`.
- func didTapMessageTopLabel(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the messageBottomLabel.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap occurred.
- ///
- /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
- /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
- /// method `messageForItem(at:indexPath:messagesCollectionView)`.
- func didTapMessageBottomLabel(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the accessoryView.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap occurred.
- ///
- /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
- /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
- /// method `messageForItem(at:indexPath:messagesCollectionView)`.
- func didTapAccessoryView(in cell: MessageCollectionViewCell)
+ /// Triggered when a tap occurs on the play button from audio cell.
+ ///
+ /// - Parameters:
+ /// - cell: The audio cell where the touch occurred.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didTapPlayButton(in cell: AudioMessageCell)
+ /// Triggered when audio player start playing audio.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the audio sound is playing.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didStartAudio(in cell: AudioMessageCell)
+
+ /// Triggered when audio player pause audio.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the audio sound is paused.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didPauseAudio(in cell: AudioMessageCell)
+
+ /// Triggered when audio player stoped audio.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the audio sound is stoped.
+ ///
+ /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
+ /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
+ /// method `messageForItem(at:indexPath:messagesCollectionView)`.
+ func didStopAudio(in cell: AudioMessageCell)
}
-public extension MessageCellDelegate {
+extension MessageCellDelegate {
+ public func didTapBackground(in _: MessageCollectionViewCell) { }
+
+ public func didTapMessage(in _: MessageCollectionViewCell) { }
+
+ public func didTapAvatar(in _: MessageCollectionViewCell) { }
+
+ public func didTapCellTopLabel(in _: MessageCollectionViewCell) { }
+
+ public func didTapCellBottomLabel(in _: MessageCollectionViewCell) { }
+
+ public func didTapMessageTopLabel(in _: MessageCollectionViewCell) { }
+
+ public func didTapImage(in _: MessageCollectionViewCell) { }
+
+ public func didTapPlayButton(in _: AudioMessageCell) { }
- func didTapMessage(in cell: MessageCollectionViewCell) {}
+ public func didStartAudio(in _: AudioMessageCell) { }
- func didTapAvatar(in cell: MessageCollectionViewCell) {}
+ public func didPauseAudio(in _: AudioMessageCell) { }
- func didTapCellTopLabel(in cell: MessageCollectionViewCell) {}
+ public func didStopAudio(in _: AudioMessageCell) { }
- func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {}
+ public func didTapMessageBottomLabel(in _: MessageCollectionViewCell) { }
- func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {}
-
- func didTapAccessoryView(in cell: MessageCollectionViewCell) {}
+ public func didTapAccessoryView(in _: MessageCollectionViewCell) { }
}
diff --git a/Sources/Protocols/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift
index e84ea622d..a1b7da6e5 100644
--- a/Sources/Protocols/MessageLabelDelegate.swift
+++ b/Sources/Protocols/MessageLabelDelegate.swift
@@ -1,74 +1,95 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+// MARK: - MessageLabelDelegate
+
/// A protocol used to handle tap events on detected text.
public protocol MessageLabelDelegate: AnyObject {
+ /// Triggered when a tap occurs on a detected address.
+ ///
+ /// - Parameters:
+ /// - addressComponents: The components of the selected address.
+ func didSelectAddress(_ addressComponents: [String: String])
+
+ /// Triggered when a tap occurs on a detected date.
+ ///
+ /// - Parameters:
+ /// - date: The selected date.
+ func didSelectDate(_ date: Date)
+
+ /// Triggered when a tap occurs on a detected phone number.
+ ///
+ /// - Parameters:
+ /// - phoneNumber: The selected phone number.
+ func didSelectPhoneNumber(_ phoneNumber: String)
+
+ /// Triggered when a tap occurs on a detected URL.
+ ///
+ /// - Parameters:
+ /// - url: The selected URL.
+ func didSelectURL(_ url: URL)
+
+ /// Triggered when a tap occurs on detected transit information.
+ ///
+ /// - Parameters:
+ /// - transitInformation: The selected transit information.
+ func didSelectTransitInformation(_ transitInformation: [String: String])
+
+ /// Triggered when a tap occurs on a mention
+ ///
+ /// - Parameters:
+ /// - mention: The selected mention
+ func didSelectMention(_ mention: String)
+
+ /// Triggered when a tap occurs on a hashtag
+ ///
+ /// - Parameters:
+ /// - mention: The selected hashtag
+ func didSelectHashtag(_ hashtag: String)
+
+ /// Triggered when a tap occurs on a custom regular expression
+ ///
+ /// - Parameters:
+ /// - pattern: the pattern of the regular expression
+ /// - match: part that match with the regular expression
+ func didSelectCustom(_ pattern: String, match: String?)
+}
- /// Triggered when a tap occurs on a detected address.
- ///
- /// - Parameters:
- /// - addressComponents: The components of the selected address.
- func didSelectAddress(_ addressComponents: [String: String])
-
- /// Triggered when a tap occurs on a detected date.
- ///
- /// - Parameters:
- /// - date: The selected date.
- func didSelectDate(_ date: Date)
-
- /// Triggered when a tap occurs on a detected phone number.
- ///
- /// - Parameters:
- /// - phoneNumber: The selected phone number.
- func didSelectPhoneNumber(_ phoneNumber: String)
-
- /// Triggered when a tap occurs on a detected URL.
- ///
- /// - Parameters:
- /// - url: The selected URL.
- func didSelectURL(_ url: URL)
-
- /// Triggered when a tap occurs on detected transit information.
- ///
- /// - Parameters:
- /// - transitInformation: The selected transit information.
- func didSelectTransitInformation(_ transitInformation: [String: String])
+extension MessageLabelDelegate {
+ public func didSelectAddress(_: [String: String]) { }
-}
+ public func didSelectDate(_: Date) { }
-public extension MessageLabelDelegate {
+ public func didSelectPhoneNumber(_: String) { }
- func didSelectAddress(_ addressComponents: [String: String]) {}
+ public func didSelectURL(_: URL) { }
- func didSelectDate(_ date: Date) {}
+ public func didSelectTransitInformation(_: [String: String]) { }
- func didSelectPhoneNumber(_ phoneNumber: String) {}
+ public func didSelectMention(_: String) { }
- func didSelectURL(_ url: URL) {}
-
- func didSelectTransitInformation(_ transitInformation: [String: String]) {}
+ public func didSelectHashtag(_: String) { }
+ public func didSelectCustom(_: String, match _: String?) { }
}
diff --git a/Sources/Protocols/MessageType.swift b/Sources/Protocols/MessageType.swift
index 627b7f505..d7c91786a 100644
--- a/Sources/Protocols/MessageType.swift
+++ b/Sources/Protocols/MessageType.swift
@@ -1,43 +1,39 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
/// A standard protocol representing a message.
/// Use this protocol to create your own message object to be used by MessageKit.
public protocol MessageType {
+ /// The sender of the message.
+ var sender: SenderType { get }
- /// The sender of the message.
- var sender: Sender { get }
+ /// The unique identifier for the message.
+ var messageId: String { get }
- /// The unique identifier for the message.
- var messageId: String { get }
-
- /// The date the message was sent.
- var sentDate: Date { get }
-
- /// The kind of message and its underlying kind.
- var kind: MessageKind { get }
+ /// The date the message was sent.
+ var sentDate: Date { get }
+ /// The kind of message and its underlying kind.
+ var kind: MessageKind { get }
}
diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift
index a9d35d804..831d1cf2f 100644
--- a/Sources/Protocols/MessagesDataSource.swift
+++ b/Sources/Protocols/MessagesDataSource.swift
@@ -1,133 +1,265 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
+// MARK: - MessagesDataSource
+
/// An object that adopts the `MessagesDataSource` protocol is responsible for providing
/// the data required by a `MessagesCollectionView`.
+@MainActor
public protocol MessagesDataSource: AnyObject {
+ /// The `SenderType` of new messages in the `MessagesCollectionView`.
+ var currentSender: SenderType { get }
+
+ /// A helper method to determine if a given message is from the current `SenderType`.
+ ///
+ /// - Parameters:
+ /// - message: The message to check if it was sent by the current `SenderType`.
+ ///
+ /// - Note:
+ /// The default implementation of this method checks for equality between
+ /// the message's `SenderType` and the current `SenderType`.
+ func isFromCurrentSender(message: MessageType) -> Bool
+
+ /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed.
+ func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType
+
+ /// The number of sections to be displayed in the `MessagesCollectionView`.
+ ///
+ /// - Parameters:
+ /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
+ func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int
+
+ /// The number of cells to be displayed in the `MessagesCollectionView`.
+ ///
+ /// - Parameters:
+ /// - section: The number of the section in which the cells will be displayed.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
+ /// - Note:
+ /// The default implementation of this method returns 1. Putting each message in its own section.
+ func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int
+
+ /// The attributed text to be used for cell's top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for cell's bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for message bubble's top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for cell's bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for cell's timestamp label.
+ /// The timestamp label is shown when showMessageTimestampOnSwipeLeft is enabled by swiping left over the chat controller.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// Text collectionView cell for message with `text`, `attributedText`, `emoji` message types.
+ ///
+ /// - Parameters:
+ /// - message: The `text`, `attributedText`, `emoji` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func textCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Photo or Video collectionView cell for message with `photo`, `video` message types.
+ ///
+ /// - Parameters:
+ /// - message: The `photo`, `video` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func photoCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Location collectionView cell for message with `location` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `location` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func locationCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Audio collectionView cell for message with `audio` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `audio` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func audioCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Contact collectionView cell for message with `contact` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `contact` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func contactCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
- /// The `Sender` of new messages in the `MessagesCollectionView`.
- func currentSender() -> Sender
-
- /// A helper method to determine if a given message is from the current `Sender`.
- ///
- /// - Parameters:
- /// - message: The message to check if it was sent by the current `Sender`.
- ///
- /// - Note:
- /// The default implementation of this method checks for equality between
- /// the message's `Sender` and the current `Sender`.
- func isFromCurrentSender(message: MessageType) -> Bool
-
- /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`.
- ///
- /// - Parameters:
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed.
- func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType
-
- /// The number of sections to be displayed in the `MessagesCollectionView`.
- ///
- /// - Parameters:
- /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
- func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int
-
- /// The number of cells to be displayed in the `MessagesCollectionView`.
- ///
- /// - Parameters:
- /// - section: The number of the section in which the cells will be displayed.
- /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
- /// - Note:
- /// The default implementation of this method returns 1. Putting each message in its own section.
- func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int
-
- /// The attributed text to be used for cell's top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// The attributed text to be used for message bubble's top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// The attributed text to be used for cell's bottom label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// Custom collectionView cell for message with `custom` message type.
- ///
- /// - Parameters:
- /// - message: The `custom` message type
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// This method will call fatalError() on default. You must override this method if you are using MessageKind.custom messages.
- func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
+ /// Custom collectionView cell for message with `custom` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `custom` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will call fatalError() on default. You must override this method if you are using MessageKind.custom messages.
+ func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell
+
+ /// Typing indicator cell used when the indicator is set to be shown
+ ///
+ /// - Parameters:
+ /// - indexPath: The index path to dequeue the cell at
+ /// - messagesCollectionView: The `MessagesCollectionView` the cell is to be rendered in
+ /// - Returns: A `UICollectionViewCell` that indicates a user is typing
+ func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
}
-public extension MessagesDataSource {
-
- func isFromCurrentSender(message: MessageType) -> Bool {
- return message.sender == currentSender()
- }
-
- func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int {
- return 1
- }
-
- func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return nil
- }
-
- func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return nil
- }
-
- func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return nil
- }
-
- func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell {
- fatalError(MessageKitError.customDataUnresolvedCell)
- }
+extension MessagesDataSource {
+ public func isFromCurrentSender(message: MessageType) -> Bool {
+ message.sender.senderId == currentSender.senderId
+ }
+
+ public func numberOfItems(inSection _: Int, in _: MessagesCollectionView) -> Int {
+ 1
+ }
+
+ public func cellTopLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func cellBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func messageTopLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func messageBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func messageTimestampLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
+ let sentDate = message.sentDate
+ let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate)
+ let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10)
+ let timeLabelColor: UIColor
+ timeLabelColor = .systemGray
+ return NSAttributedString(
+ string: sentDateString,
+ attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor])
+ }
+
+ public func textCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func photoCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func locationCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func audioCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func contactCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func customCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell {
+ fatalError(MessageKitError.customDataUnresolvedCell)
+ }
+
+ public func typingIndicator(
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell
+ {
+ messagesCollectionView.dequeueReusableCell(TypingIndicatorCell.self, for: indexPath)
+ }
}
diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift
index 2af2eac5a..81a5d6722 100644
--- a/Sources/Protocols/MessagesDisplayDelegate.swift
+++ b/Sources/Protocols/MessagesDisplayDelegate.swift
@@ -1,246 +1,408 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import MapKit
+import UIKit
+
+// MARK: - MessagesDisplayDelegate
/// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageContentCell`.
+@MainActor
public protocol MessagesDisplayDelegate: AnyObject {
-
- // MARK: - All Messages
-
- /// Specifies the `MessageStyle` to be used for a `MessageContainerView`.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is `MessageStyle.bubble`.
- func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle
-
- /// Specifies the background color of the `MessageContainerView`.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value is `UIColor.clear` for emoji messages.
- /// For all other `MessageKind` cases, the color depends on the `Sender`.
- ///
- /// Current sender: Green
- ///
- /// All other senders: Gray
- func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
-
- /// The section header to use for a given `IndexPath`.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this header.
- /// - indexPath: The `IndexPath` of the header.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
- func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
-
- /// The section footer to use for a given `IndexPath`.
- ///
- /// - Parameters:
- /// - indexPath: The `IndexPath` of the footer.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
- func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
-
- /// Used to configure the `AvatarView`βs image in a `MessageContentCell` class.
- ///
- /// - Parameters:
- /// - avatarView: The `AvatarView` of the cell.
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default image configured by this method is `?`.
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
-
- /// Used to configure the `AccessoryView` in a `MessageContentCell` class.
- ///
- /// - Parameters:
- /// - accessoryView: The `AccessoryView` of the cell.
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default image configured by this method is `?`.
- func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
-
- // MARK: - Text Messages
-
- /// Specifies the color of the text for a `TextMessageCell`.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.text` to which the color will apply.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is determined by the messages `Sender`.
- ///
- /// Current sender: UIColor.white
- ///
- /// All other senders: UIColor.darkText
- func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
-
- /// Specifies the `DetectorType`s to check for the `MessageType`'s text against.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// This method returns an empty array by default.
- func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType]
-
- /// Specifies the attributes for a given `DetectorType`
- ///
- /// - Parameters:
- /// - detector: The `DetectorType` for the applied attributes.
- /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
- /// - indexPath: The `IndexPath` of the cell.
- func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any]
-
- // MARK: - Location Messages
-
- /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.location`.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
- /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style.
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions
-
- /// Used to configure the annoation view of the map image on the given location message.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.location`.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
- /// - Returns: The `MKAnnotationView` to use as the annotation view.
- func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView?
-
- /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message.
- /// The animation block is called with the `UIImageView` to be animated.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.location`.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
- /// - Returns: The animation block to use to apply the location image.
- func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)?
-
- // MARK: - Media Messages
-
- /// Used to configure the `UIImageView` of a `MediaMessageCell.
- ///
- /// - Parameters:
- /// - imageView: The `UIImageView` of the cell.
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
-
+ // MARK: - All Messages
+
+ /// Specifies the `MessageStyle` to be used for a `MessageContainerView`.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is `MessageStyle.bubble`.
+ func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> MessageStyle
+
+ /// Specifies the background color of the `MessageContainerView`.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value is `UIColor.clear` for emoji messages.
+ /// For all other `MessageKind` cases, the color depends on the `SenderType`.
+ ///
+ /// Current sender: Green
+ ///
+ /// All other senders: Gray
+ func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+
+ /// The section header to use for a given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this header.
+ /// - indexPath: The `IndexPath` of the header.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
+ func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
+
+ /// The section footer to use for a given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - indexPath: The `IndexPath` of the footer.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
+ func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
+
+ /// Used to configure the `AvatarView`βs image in a `MessageContentCell` class.
+ ///
+ /// - Parameters:
+ /// - avatarView: The `AvatarView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default image configured by this method is `?`.
+ func configureAvatarView(
+ _ avatarView: AvatarView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+
+ /// Used to configure the `AccessoryView` in a `MessageContentCell` class.
+ ///
+ /// - Parameters:
+ /// - accessoryView: The `AccessoryView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default image configured by this method is `?`.
+ func configureAccessoryView(
+ _ accessoryView: UIView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+
+ // MARK: - Text Messages
+
+ /// Specifies the color of the text for a `TextMessageCell`.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.text` to which the color will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is determined by the messages `SenderType`.
+ ///
+ /// Current sender: UIColor.white
+ ///
+ /// All other senders: UIColor.darkText
+ func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+
+ /// Specifies the `DetectorType`s to check for the `MessageType`'s text against.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method returns an empty array by default.
+ func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> [DetectorType]
+
+ /// Specifies the attributes for a given `DetectorType`
+ ///
+ /// - Parameters:
+ /// - detector: The `DetectorType` for the applied attributes.
+ /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath)
+ -> [NSAttributedString.Key: Any]
+
+ // MARK: - Location Messages
+
+ /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.location`.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
+ /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style.
+ func snapshotOptionsForLocation(
+ message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions
+
+ /// Used to configure the annotation view of the map image on the given location message.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.location`.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
+ /// - Returns: The `MKAnnotationView` to use as the annotation view.
+ func annotationViewForLocation(
+ message: MessageType,
+ at indexPath: IndexPath,
+ in messageCollectionView: MessagesCollectionView) -> MKAnnotationView?
+
+ /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message.
+ /// The animation block is called with the `UIImageView` to be animated.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.location`.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
+ /// - Returns: The animation block to use to apply the location image.
+ func animationBlockForLocation(
+ message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)?
+
+ // MARK: - Media Messages
+
+ /// Used to configure the `UIImageView` of a `MediaMessageCell`.
+ ///
+ /// - Parameters:
+ /// - imageView: The `UIImageView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ func configureMediaMessageImageView(
+ _ imageView: UIImageView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+
+ // MARK: - Audio Message
+
+ /// Used to configure the audio cell UI:
+ /// 1. play button selected state;
+ /// 2. progressView progress;
+ /// 3. durationLabel text;
+ ///
+ /// - Parameters:
+ /// - cell: The `AudioMessageCell` that needs to be configure.
+ /// - message: The `MessageType` that configures the cell.
+ ///
+ /// - Note:
+ /// This protocol method is called by MessageKit every time an audio cell needs to be configure
+ func configureAudioCell(_ cell: AudioMessageCell, message: MessageType)
+
+ /// Specifies the tint color of play button and progress bar for an `AudioMessageCell`.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.audio` to which the color will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is UIColor.sendButtonBlue
+ func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+
+ /// Used to format the audio sound duration in a readable string
+ ///
+ /// - Parameters:
+ /// - duration: The audio sound duration is seconds.
+ /// - audioCell: The `AudioMessageCell` that ask for formated duration.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value is computed like fallow:
+ /// 1. return the time as 0:ss if duration is up to 59 seconds (e.g. 0:03 means 0 minutes and 3 seconds)
+ /// 2. return the time as m:ss if duration is greater than 59 and lower than 3600 (e.g. 12:23 means 12 minutes and 23 seconds)
+ /// 3. return the time as h:mm:ss for anything longer that 3600 seconds (e.g. 1:19:08 means 1 hour 19 minutes and 8 seconds)
+ func audioProgressTextFormat(
+ _ duration: Float,
+ for audioCell: AudioMessageCell,
+ in messageCollectionView: MessagesCollectionView) -> String
+
+ /// Used to configure the `UIImageView` of a `LinkPreviewMessageCell`.
+ /// - Parameters:
+ /// - imageView: The `UIImageView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ func configureLinkPreviewImageView(
+ _ imageView: UIImageView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
}
-public extension MessagesDisplayDelegate {
-
- // MARK: - All Messages Defaults
-
- func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
- return .bubble
+extension MessagesDisplayDelegate {
+ // MARK: - All Messages Defaults
+
+ public func messageStyle(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
+ .bubble
+ }
+
+ public func backgroundColor(
+ for message: MessageType,
+ at _: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+ {
+ switch message.kind {
+ case .emoji:
+ return .clear
+ default:
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ return .white
+ }
+ return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageBackground : .incomingMessageBackground
}
-
- func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
-
- switch message.kind {
- case .emoji:
- return .clear
- default:
- guard let dataSource = messagesCollectionView.messagesDataSource else { return .white }
- return dataSource.isFromCurrentSender(message: message) ? .outgoingGreen : .incomingGray
- }
+ }
+
+ public func messageHeaderView(
+ for indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> MessageReusableView
+ {
+ messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath)
+ }
+
+ public func messageFooterView(
+ for indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> MessageReusableView
+ {
+ messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath)
+ }
+
+ public func configureAvatarView(_ avatarView: AvatarView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
+ avatarView.initials = "?"
+ }
+
+ public func configureAccessoryView(_: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) { }
+
+ // MARK: - Text Messages Defaults
+
+ public func textColor(
+ for message: MessageType,
+ at _: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+ {
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView {
- return messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath)
+ return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageLabel : .incomingMessageLabel
+ }
+
+ public func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
+ []
+ }
+
+ public func detectorAttributes(for _: DetectorType, and _: MessageType, at _: IndexPath) -> [NSAttributedString.Key: Any] {
+ MessageLabel.defaultAttributes
+ }
+
+ // MARK: - Location Messages Defaults
+
+ public func snapshotOptionsForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LocationMessageSnapshotOptions
+ {
+ LocationMessageSnapshotOptions()
+ }
+
+ public func annotationViewForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> MKAnnotationView?
+ {
+ MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
+ }
+
+ public func animationBlockForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) -> ((UIImageView) -> Void)?
+ {
+ nil
+ }
+
+ // MARK: - Media Message Defaults
+
+ public func configureMediaMessageImageView(
+ _: UIImageView,
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) { }
+
+ // MARK: - Audio Message Defaults
+
+ public func configureAudioCell(_: AudioMessageCell, message _: MessageType) { }
+
+ public func audioTintColor(
+ for message: MessageType,
+ at _: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+ {
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView {
- return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath)
- }
-
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- avatarView.initials = "?"
+ return dataSource.isFromCurrentSender(message: message) ? .outgoingAudioMessageTint : .incomingAudioMessageTint
+ }
+
+ public func audioProgressTextFormat(_ duration: Float, for _: AudioMessageCell, in _: MessagesCollectionView) -> String {
+ var returnValue = "0:00"
+ // print the time as 0:ss if duration is up to 59 seconds
+ // print the time as m:ss if duration is up to 59:59 seconds
+ // print the time as h:mm:ss for anything longer
+ if duration < 60 {
+ returnValue = String(format: "0:%.02d", Int(duration.rounded(.up)))
+ } else if duration < 3600 {
+ returnValue = String(format: "%.02d:%.02d", Int(duration / 60), Int(duration) % 60)
+ } else if duration.isFinite {
+ let hours = Int(duration / 3600)
+ let remainingMinutesInSeconds = Int(duration) - hours * 3600
+ returnValue = String(
+ format: "%.02d:%.02d:%.02d",
+ hours,
+ Int(remainingMinutesInSeconds / 60),
+ Int(remainingMinutesInSeconds) % 60)
}
+ return returnValue
+ }
- func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {}
-
- // MARK: - Text Messages Defaults
+ // MARK: - LinkPreview Message Defaults
- func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- return dataSource.isFromCurrentSender(message: message) ? .white : .darkText
- }
-
- func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
- return []
- }
-
- func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
- return MessageLabel.defaultAttributes
- }
-
- // MARK: - Location Messages Defaults
-
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
- return LocationMessageSnapshotOptions()
- }
-
- func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
- return MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
- }
-
- func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
- return nil
- }
-
- // MARK: - Media Message Defaults
-
- func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- }
+ public func configureLinkPreviewImageView(
+ _: UIImageView,
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) { }
}
diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift
index 4dd75743f..015c45c30 100644
--- a/Sources/Protocols/MessagesLayoutDelegate.swift
+++ b/Sources/Protocols/MessagesLayoutDelegate.swift
@@ -1,121 +1,413 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
+
+// MARK: - MessagesLayoutDelegate
/// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine
/// the size and layout of a `MessageCollectionViewCell` and its contents.
+@MainActor
public protocol MessagesLayoutDelegate: AnyObject {
+ /// Specifies the size to use for a header view.
+ ///
+ /// - Parameters:
+ /// - section: The section number of the header.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is a size of `GGSize.zero`.
+ func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
+
+ /// Specifies the size to use for a footer view.
+ ///
+ /// - Parameters:
+ /// - section: The section number of the footer.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is a size of `GGSize.zero`.
+ func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
+
+ /// Specifies the size to use for a typing indicator view.
+ ///
+ /// - Parameters:
+ /// - layout: The `MessagesCollectionViewFlowLayout` layout.
+ ///
+ /// - Note:
+ /// The default value returned by this method is the width of the `messagesCollectionView` minus insets and a height of 62.
+ func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize
+
+ /// Specifies the top inset to use for a typing indicator view.
+ ///
+ /// - Parameters:
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this view will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is a top inset of 15.
+ func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the height for the `MessageContentCell`'s top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> CGFloat
+
+ /// Specifies the height for the `MessageContentCell`'s bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func cellBottomLabelHeight(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the height for the message bubble's top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func messageTopLabelHeight(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the label alignment for the message bubble's top label.
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ /// - Returns: Optional LabelAlignment for the message bubble's top label. If nil is returned or the delegate method is not implemented,
+ /// alignment from MessageSizeCalculator will be used depending if the message is outgoing or incoming
+ func messageTopLabelAlignment(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> LabelAlignment?
+
+ /// Specifies the height for the `MessageContentCell`'s bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func messageBottomLabelHeight(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the label alignment for the message bubble's bottom label.
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ /// - Returns: Optional LabelAlignment for the message bubble's bottom label. If nil is returned or the delegate method is not implemented,
+ /// alignment from MessageSizeCalculator will be used depending if the message is outgoing or incoming
+ func messageBottomLabelAlignment(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> LabelAlignment?
+
+ /// Specifies the size for the `MessageContentCell`'s avatar image view.
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ /// - Returns: Optional CGSize for the avatar image view. If nil is returned or delegate method is not implemented,
+ /// size from `MessageSizeCalculator`'s `incomingAvatarSize` or `outgoingAvatarSize` will be used depending if the message is outgoing or incoming
+ func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> CGSize?
+
+ /// Text cell size calculator for messages with MessageType.text.
+ ///
+ /// - Parameters:
+ /// - message: The text message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.text.
+ func textCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Attributed text cell size calculator for messages with MessageType.attributedText.
+ ///
+ /// - Parameters:
+ /// - message: The attributedText message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.attributedText.
+ func attributedTextCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Emoji cell size calculator for messages with MessageType.emoji.
+ ///
+ /// - Parameters:
+ /// - message: The emoji message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.emoji.
+ func emojiCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Photo cell size calculator for messages with MessageType.photo.
+ ///
+ /// - Parameters:
+ /// - message: The photo message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.text.
+ func photoCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Video cell size calculator for messages with MessageType.video.
+ ///
+ /// - Parameters:
+ /// - message: The video message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.video.
+ func videoCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Location cell size calculator for messages with MessageType.location.
+ ///
+ /// - Parameters:
+ /// - message: The location message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.location.
+ func locationCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Audio cell size calculator for messages with MessageType.audio.
+ ///
+ /// - Parameters:
+ /// - message: The audio message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.audio.
+ func audioCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Contact cell size calculator for messages with MessageType.contact.
+ ///
+ /// - Parameters:
+ /// - message: The contact message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.contact.
+ func contactCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
- /// Specifies the size to use for a header view.
- ///
- /// - Parameters:
- /// - section: The section number of the header.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is a size of `GGSize.zero`.
- func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
-
- /// Specifies the size to use for a footer view.
- ///
- /// - Parameters:
- /// - section: The section number of the footer.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is a size of `GGSize.zero`.
- func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
-
- /// Specifies the height for the `MessageContentCell`'s top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is zero.
- func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Specifies the height for the message bubble's top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is zero.
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Specifies the height for the `MessageContentCell`'s bottom label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is zero.
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Custom cell size calculator for messages with MessageType.custom.
- ///
- /// - Parameters:
- /// - message: The custom message
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom.
- func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
+ /// Custom cell size calculator for messages with MessageType.custom.
+ ///
+ /// - Parameters:
+ /// - message: The custom message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom.
+ func customCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
}
-public extension MessagesLayoutDelegate {
-
- func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize {
- return .zero
- }
-
- func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize {
- return .zero
- }
-
- func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0
- }
-
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0
- }
-
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0
- }
-
- func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator {
- fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)")
- }
+extension MessagesLayoutDelegate {
+ public func headerViewSize(for _: Int, in _: MessagesCollectionView) -> CGSize {
+ .zero
+ }
+
+ public func footerViewSize(for _: Int, in _: MessagesCollectionView) -> CGSize {
+ .zero
+ }
+
+ public func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize {
+ let collectionViewWidth = layout.messagesCollectionView.bounds.width
+ let contentInset = layout.messagesCollectionView.contentInset
+ let inset = layout.sectionInset.horizontal + contentInset.horizontal
+ return CGSize(width: collectionViewWidth - inset, height: 62)
+ }
+
+ public func typingIndicatorViewTopInset(in _: MessagesCollectionView) -> CGFloat {
+ 15
+ }
+
+ public func cellTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func cellBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func messageTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func messageTopLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment? {
+ nil
+ }
+
+ public func messageBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func avatarSize(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGSize? {
+ nil
+ }
+
+ public func messageBottomLabelAlignment(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LabelAlignment?
+ {
+ nil
+ }
+
+ public func textCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator? {
+ nil
+ }
+
+ public func attributedTextCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func emojiCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func photoCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func videoCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func locationCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func audioCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func contactCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func customCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator
+ {
+ fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)")
+ }
}
diff --git a/Sources/Protocols/SenderType.swift b/Sources/Protocols/SenderType.swift
new file mode 100644
index 000000000..e22605f0e
--- /dev/null
+++ b/Sources/Protocols/SenderType.swift
@@ -0,0 +1,35 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+
+/// A standard protocol representing a sender.
+/// Use this protocol to adhere a object as the sender of a MessageType
+public protocol SenderType {
+ /// The unique String identifier for the sender.
+ ///
+ /// Note: This value must be unique across all senders.
+ var senderId: String { get }
+
+ /// The display name of a sender.
+ var displayName: String { get }
+}
diff --git a/Sources/Supporting/Info.plist b/Sources/Supporting/Info.plist
index 60b9c008e..ca23c84f4 100644
--- a/Sources/Supporting/Info.plist
+++ b/Sources/Supporting/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 1.0.0
+ $(MARKETING_VERSION)
CFBundleSignature
????
CFBundleVersion
diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift
deleted file mode 100644
index 735409e32..000000000
--- a/Sources/Supporting/MessageKit+Availability.swift
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import Foundation
-
-public extension MessagesLayoutDelegate {
-
- func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
- fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition {
- fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
- fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0")
- }
-
- func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
- fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment {
- fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment {
- fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func shouldCacheLayoutAttributes(for message: MessageType) -> Bool {
- fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.")
- }
-}
diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift
index cf3c14561..3fe8ed56e 100644
--- a/Sources/Views/AvatarView.swift
+++ b/Sources/Views/AvatarView.swift
@@ -1,197 +1,218 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+import UIKit
+
+// MARK: - AvatarView
open class AvatarView: UIImageView {
+ // MARK: Lifecycle
- // MARK: - Properties
-
- open var initials: String? {
- didSet {
- setImageFrom(initials: initials)
- }
- }
+ // MARK: - Initializers
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ prepareView()
+ }
- open var placeholderFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) {
- didSet {
- setImageFrom(initials: initials)
- }
- }
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ prepareView()
+ }
- open var placeholderTextColor: UIColor = .white {
- didSet {
- setImageFrom(initials: initials)
- }
- }
+ convenience public init() {
+ self.init(frame: .zero)
+ prepareView()
+ }
- open var fontMinimumScaleFactor: CGFloat = 0.5
+ // MARK: Open
- open var adjustsFontSizeToFitWidth = true
+ open var fontMinimumScaleFactor: CGFloat = 0.5
- private var minimumFontSize: CGFloat {
- return placeholderFont.pointSize * fontMinimumScaleFactor
- }
+ open var adjustsFontSizeToFitWidth = true
- private var radius: CGFloat?
+ // MARK: - Properties
- // MARK: - Overridden Properties
- open override var frame: CGRect {
- didSet {
- setCorner(radius: self.radius)
- }
+ open var initials: String? {
+ didSet {
+ setImageFrom(initials: initials)
}
+ }
- open override var bounds: CGRect {
- didSet {
- setCorner(radius: self.radius)
- }
- }
-
- // MARK: - Initializers
- public override init(frame: CGRect) {
- super.init(frame: frame)
- prepareView()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- prepareView()
- }
-
- convenience public init() {
- self.init(frame: .zero)
- prepareView()
- }
-
- private func setImageFrom(initials: String?) {
- guard let initials = initials else { return }
- image = getImageFrom(initials: initials)
+ open var placeholderFont = UIFont.preferredFont(forTextStyle: .caption1) {
+ didSet {
+ setImageFrom(initials: initials)
}
+ }
- private func getImageFrom(initials: String) -> UIImage {
- let width = frame.width
- let height = frame.height
- if width == 0 || height == 0 {return UIImage()}
- var font = placeholderFont
-
- _ = UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, UIScreen.main.scale)
- defer { UIGraphicsEndImageContext() }
- let context = UIGraphicsGetCurrentContext()!
-
- //// Text Drawing
- let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height)
- let initialsText = NSAttributedString(string: initials, attributes: [.font: font])
- if adjustsFontSizeToFitWidth,
- initialsText.width(considering: textRect.height) > textRect.width {
- let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height)
- font = placeholderFont.withSize(newFontSize)
- }
-
- let textStyle = NSMutableParagraphStyle()
- textStyle.alignment = .center
- let textFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: placeholderTextColor, NSAttributedString.Key.paragraphStyle: textStyle]
-
- let textTextHeight: CGFloat = initials.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height
- context.saveGState()
- context.clip(to: textRect)
- initials.draw(in: CGRect(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight), withAttributes: textFontAttributes)
- context.restoreGState()
- guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext() else { assertionFailure("Could not create image from context"); return UIImage()}
- return renderedImage
+ open var placeholderTextColor: UIColor = .white {
+ didSet {
+ setImageFrom(initials: initials)
}
-
- /**
- Recursively find the biggest size to fit the text with a given width and height
- */
- private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat {
- let attributedText = NSAttributedString(string: text, attributes: [.font: font])
- if attributedText.width(considering: height) > width {
- let newFont = font.withSize(font.pointSize - 1)
- if newFont.pointSize > minimumFontSize {
- return font.pointSize
- } else {
- return calculateFontSize(text: text, font: newFont, width: width, height: height)
- }
- }
+ }
+
+ // MARK: - Overridden Properties
+ open override var frame: CGRect {
+ didSet {
+ setCorner(radius: self.radius)
+ }
+ }
+
+ open override var bounds: CGRect {
+ didSet {
+ setCorner(radius: self.radius)
+ }
+ }
+
+ // MARK: - Open setters
+
+ open func set(avatar: Avatar) {
+ if let image = avatar.image {
+ self.image = image
+ } else {
+ initials = avatar.initials
+ }
+ }
+
+ open func setCorner(radius: CGFloat?) {
+ guard let radius = radius else {
+ // if corner radius not set default to Circle
+ let cornerRadius = min(frame.width, frame.height)
+ layer.cornerRadius = cornerRadius / 2
+ return
+ }
+ self.radius = radius
+ layer.cornerRadius = radius
+ }
+
+ // MARK: Internal
+
+ // MARK: - Internal methods
+
+ internal func prepareView() {
+ backgroundColor = .avatarViewBackground
+ contentMode = .scaleAspectFill
+ layer.masksToBounds = true
+ clipsToBounds = true
+ setCorner(radius: nil)
+ }
+
+ // MARK: Private
+
+ private var radius: CGFloat?
+
+ private var minimumFontSize: CGFloat {
+ placeholderFont.pointSize * fontMinimumScaleFactor
+ }
+
+ private func setImageFrom(initials: String?) {
+ guard let initials = initials else { return }
+ image = getImageFrom(initials: initials)
+ }
+
+ private func getImageFrom(initials: String) -> UIImage {
+ let width = frame.width
+ let height = frame.height
+ if width == 0 || height == 0 { return UIImage() }
+ var font = placeholderFont
+
+ UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, UIScreen.main.scale)
+ defer { UIGraphicsEndImageContext() }
+ let context = UIGraphicsGetCurrentContext()!
+
+ //// Text Drawing
+ let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height)
+ let initialsText = NSAttributedString(string: initials, attributes: [.font: font])
+ if
+ adjustsFontSizeToFitWidth,
+ initialsText.width(considering: textRect.height) > textRect.width
+ {
+ let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height)
+ font = placeholderFont.withSize(newFontSize)
+ }
+
+ let textStyle = NSMutableParagraphStyle()
+ textStyle.alignment = .center
+ let textFontAttributes: [NSAttributedString.Key: Any] = [
+ NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor: placeholderTextColor,
+ NSAttributedString.Key.paragraphStyle: textStyle,
+ ]
+
+ let textTextHeight: CGFloat = initials.boundingRect(
+ with: CGSize(width: textRect.width, height: CGFloat.infinity),
+ options: .usesLineFragmentOrigin,
+ attributes: textFontAttributes,
+ context: nil).height
+ context.saveGState()
+ context.clip(to: textRect)
+ initials.draw(
+ in: CGRect(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight),
+ withAttributes: textFontAttributes)
+ context.restoreGState()
+ guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext()
+ else { assertionFailure("Could not create image from context"); return UIImage() }
+ return renderedImage
+ }
+
+ /// Recursively find the biggest size to fit the text with a given width and height
+ private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat {
+ let attributedText = NSAttributedString(string: text, attributes: [.font: font])
+ if attributedText.width(considering: height) > width {
+ let newFont = font.withSize(font.pointSize - 1)
+ if newFont.pointSize > minimumFontSize {
return font.pointSize
- }
-
- /**
- Calculates the inner circle's width.
- Note: Assumes corner radius cannot be more than width/2 (this creates circle).
- */
- private func calculateTextRect(outerViewWidth: CGFloat, outerViewHeight: CGFloat) -> CGRect {
- guard outerViewWidth > 0 else {
- return CGRect.zero
- }
- let shortEdge = min(outerViewHeight, outerViewWidth)
- // Converts degree to radian degree and calculate the
- // Assumes, it is a perfect circle based on the shorter part of ellipsoid
- // calculate a rectangle
- let w = shortEdge * sin(CGFloat(45).degreesToRadians) * 2
- let h = shortEdge * cos(CGFloat(45).degreesToRadians) * 2
- let startX = (outerViewWidth - w)/2
- let startY = (outerViewHeight - h)/2
- // In case the font exactly fits to the region, put 2 pixel both left and right
- return CGRect(startX+2, startY, w-4, h)
- }
-
- // MARK: - Internal methods
-
- internal func prepareView() {
- backgroundColor = .gray
- contentMode = .scaleAspectFill
- layer.masksToBounds = true
- clipsToBounds = true
- setCorner(radius: nil)
- }
+ } else {
+ return calculateFontSize(text: text, font: newFont, width: width, height: height)
+ }
+ }
+ return font.pointSize
+ }
+
+ /// Calculates the inner circle's width.
+ /// - Note: Assumes corner radius cannot be more than width/2 (this creates circle).
+ private func calculateTextRect(outerViewWidth: CGFloat, outerViewHeight: CGFloat) -> CGRect {
+ guard outerViewWidth > 0 else {
+ return CGRect.zero
+ }
+ let shortEdge = min(outerViewHeight, outerViewWidth)
+ // Converts degree to radian degree and calculate the
+ // Assumes, it is a perfect circle based on the shorter part of ellipsoid
+ // calculate a rectangle
+ let w = shortEdge * sin(CGFloat(45).degreesToRadians) * 2
+ let h = shortEdge * cos(CGFloat(45).degreesToRadians) * 2
+ let startX = (outerViewWidth - w) / 2
+ let startY = (outerViewHeight - h) / 2
+ // In case the font exactly fits to the region, put 2 pixel both left and right
+ return CGRect(startX + 2, startY, w - 4, h)
+ }
+}
- // MARK: - Open setters
-
- open func set(avatar: Avatar) {
- if let image = avatar.image {
- self.image = image
- } else {
- initials = avatar.initials
- }
- }
+extension FloatingPoint {
+ // MARK: Fileprivate
- open func setCorner(radius: CGFloat?) {
- guard let radius = radius else {
- //if corner radius not set default to Circle
- let cornerRadius = min(frame.width, frame.height)
- layer.cornerRadius = cornerRadius/2
- return
- }
- self.radius = radius
- layer.cornerRadius = radius
- }
+ fileprivate var degreesToRadians: Self { self * .pi / 180 }
-}
+ // MARK: Private
-fileprivate extension FloatingPoint {
- var degreesToRadians: Self { return self * .pi / 180 }
- var radiansToDegrees: Self { return self * 180 / .pi }
+ private var radiansToDegrees: Self { self * 180 / .pi }
}
diff --git a/Sources/Views/BubbleCircle.swift b/Sources/Views/BubbleCircle.swift
new file mode 100644
index 000000000..93acf6f0b
--- /dev/null
+++ b/Sources/Views/BubbleCircle.swift
@@ -0,0 +1,48 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import UIKit
+
+/// A `UIView` subclass that maintains a mask to keep it fully circular
+open class BubbleCircle: UIView {
+ /// Lays out subviews and applies a circular mask to the layer
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+ layer.mask = roundedMask(corners: .allCorners, radius: bounds.height / 2)
+ }
+
+ /// Returns a rounded mask of the view
+ ///
+ /// - Parameters:
+ /// - corners: The corners to round
+ /// - radius: The radius of curve
+ /// - Returns: A mask
+ open func roundedMask(corners: UIRectCorner, radius: CGFloat) -> CAShapeLayer {
+ let path = UIBezierPath(
+ roundedRect: bounds,
+ byRoundingCorners: corners,
+ cornerRadii: CGSize(width: radius, height: radius))
+ let mask = CAShapeLayer()
+ mask.path = path.cgPath
+ return mask
+ }
+}
diff --git a/Sources/Views/Cells/AudioMessageCell.swift b/Sources/Views/Cells/AudioMessageCell.swift
new file mode 100644
index 000000000..e32ecd8cd
--- /dev/null
+++ b/Sources/Views/Cells/AudioMessageCell.swift
@@ -0,0 +1,162 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import AVFoundation
+import UIKit
+
+/// A subclass of `MessageContentCell` used to display video and audio messages.
+open class AudioMessageCell: MessageContentCell {
+ // MARK: Open
+
+ /// Responsible for setting up the constraints of the cell's subviews.
+ open func setupConstraints() {
+ playButton.constraint(equalTo: CGSize(width: 25, height: 25))
+ playButton.addConstraints(
+ left: messageContainerView.leftAnchor,
+ centerY: messageContainerView.centerYAnchor,
+ leftConstant: 5)
+ activityIndicatorView.addConstraints(centerY: playButton.centerYAnchor, centerX: playButton.centerXAnchor)
+ durationLabel.addConstraints(
+ right: messageContainerView.rightAnchor,
+ centerY: messageContainerView.centerYAnchor,
+ rightConstant: 15)
+ progressView.addConstraints(
+ left: playButton.rightAnchor,
+ right: durationLabel.leftAnchor,
+ centerY: messageContainerView.centerYAnchor,
+ leftConstant: 5,
+ rightConstant: 5)
+ }
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(playButton)
+ messageContainerView.addSubview(activityIndicatorView)
+ messageContainerView.addSubview(durationLabel)
+ messageContainerView.addSubview(progressView)
+ setupConstraints()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ progressView.progress = 0
+ playButton.isSelected = false
+ activityIndicatorView.stopAnimating()
+ playButton.isHidden = false
+ durationLabel.text = "0:00"
+ }
+
+ /// Handle tap gesture on contentView and its subviews.
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: self)
+ // compute play button touch area, currently play button size is (25, 25) which is hardly touchable
+ // add 10 px around current button frame and test the touch against this new frame
+ let playButtonTouchArea = CGRect(
+ playButton.frame.origin.x - 10.0,
+ playButton.frame.origin.y - 10,
+ playButton.frame.size.width + 20,
+ playButton.frame.size.height + 20)
+ let translateTouchLocation = convert(touchLocation, to: messageContainerView)
+ if playButtonTouchArea.contains(translateTouchLocation) {
+ delegate?.didTapPlayButton(in: self)
+ } else {
+ super.handleTapGesture(gesture)
+ }
+ }
+
+ // MARK: - Configure Cell
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
+ }
+
+ let playButtonLeftConstraint = messageContainerView.constraints.filter { $0.identifier == "left" }.first
+ let durationLabelRightConstraint = messageContainerView.constraints.filter { $0.identifier == "right" }.first
+
+ if !dataSource.isFromCurrentSender(message: message) {
+ playButtonLeftConstraint?.constant = 12
+ durationLabelRightConstraint?.constant = -8
+ } else {
+ playButtonLeftConstraint?.constant = 5
+ durationLabelRightConstraint?.constant = -15
+ }
+
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
+ }
+
+ let tintColor = displayDelegate.audioTintColor(for: message, at: indexPath, in: messagesCollectionView)
+ playButton.imageView?.tintColor = tintColor
+ durationLabel.textColor = tintColor
+ progressView.tintColor = tintColor
+
+ if case .audio(let audioItem) = message.kind {
+ durationLabel.text = displayDelegate.audioProgressTextFormat(
+ audioItem.duration,
+ for: self,
+ in: messagesCollectionView)
+ }
+
+ displayDelegate.configureAudioCell(self, message: message)
+ }
+
+ // MARK: Public
+
+ /// The play button view to display on audio messages.
+ public lazy var playButton: UIButton = {
+ let playButton = UIButton(type: .custom)
+ let playImage = UIImage.messageKitImageWith(type: .play)
+ let pauseImage = UIImage.messageKitImageWith(type: .pause)
+ playButton.setImage(playImage?.withRenderingMode(.alwaysTemplate), for: .normal)
+ playButton.setImage(pauseImage?.withRenderingMode(.alwaysTemplate), for: .selected)
+ return playButton
+ }()
+
+ /// The time duration label to display on audio messages.
+ public lazy var durationLabel: UILabel = {
+ let durationLabel = UILabel(frame: CGRect.zero)
+ durationLabel.textAlignment = .right
+ durationLabel.font = UIFont.systemFont(ofSize: 14)
+ durationLabel.text = "0:00"
+ return durationLabel
+ }()
+
+ public lazy var activityIndicatorView: UIActivityIndicatorView = {
+ let activityIndicatorView = UIActivityIndicatorView(style: .medium)
+ activityIndicatorView.hidesWhenStopped = true
+ activityIndicatorView.isHidden = true
+ return activityIndicatorView
+ }()
+
+ public lazy var progressView: UIProgressView = {
+ let progressView = UIProgressView(progressViewStyle: .default)
+ progressView.progress = 0.0
+ return progressView
+ }()
+}
diff --git a/Sources/Views/Cells/ContactMessageCell.swift b/Sources/Views/Cells/ContactMessageCell.swift
new file mode 100644
index 000000000..cc3e64cc0
--- /dev/null
+++ b/Sources/Views/Cells/ContactMessageCell.swift
@@ -0,0 +1,152 @@
+// MIT License
+//
+// Copyright (c) 2017-2018 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+open class ContactMessageCell: MessageContentCell {
+ // MARK: Open
+
+ // MARK: - Methods
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else {
+ return
+ }
+ nameLabel.font = attributes.messageLabelFont
+ }
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(initialsContainerView)
+ messageContainerView.addSubview(nameLabel)
+ messageContainerView.addSubview(disclosureImageView)
+ initialsContainerView.addSubview(initialsLabel)
+ setupConstraints()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ nameLabel.text = ""
+ initialsLabel.text = ""
+ }
+
+ open func setupConstraints() {
+ initialsContainerView.constraint(equalTo: CGSize(width: 26, height: 26))
+ let initialsConstraints = initialsContainerView.addConstraints(
+ left: messageContainerView.leftAnchor,
+ centerY: messageContainerView.centerYAnchor,
+ leftConstant: 5)
+ initialsConstraints.first?.identifier = ConstraintsID.initialsContainerLeftConstraint.rawValue
+ initialsContainerView.layer.cornerRadius = 13
+ initialsLabel.fillSuperview()
+ disclosureImageView.constraint(equalTo: CGSize(width: 20, height: 20))
+ let disclosureConstraints = disclosureImageView.addConstraints(
+ right: messageContainerView.rightAnchor,
+ centerY: messageContainerView.centerYAnchor,
+ rightConstant: -10)
+ disclosureConstraints.first?.identifier = ConstraintsID.disclosureRightConstraint.rawValue
+ nameLabel.addConstraints(
+ messageContainerView.topAnchor,
+ left: initialsContainerView.rightAnchor,
+ bottom: messageContainerView.bottomAnchor,
+ right: disclosureImageView.leftAnchor,
+ topConstant: 0,
+ leftConstant: 10,
+ bottomConstant: 0,
+ rightConstant: 5)
+ }
+
+ // MARK: - Configure Cell
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+ // setup data
+ guard case .contact(let contactItem) = message.kind else { fatalError("Failed decorate audio cell") }
+ nameLabel.text = contactItem.displayName
+ initialsLabel.text = contactItem.initials
+ // setup constraints
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
+ }
+ let initialsContainerLeftConstraint = messageContainerView.constraints.filter { constraint -> Bool in
+ constraint.identifier == ConstraintsID.initialsContainerLeftConstraint.rawValue
+ }.first
+ let disclosureRightConstraint = messageContainerView.constraints.filter { constraint -> Bool in
+ constraint.identifier == ConstraintsID.disclosureRightConstraint.rawValue
+ }.first
+ if dataSource.isFromCurrentSender(message: message) { // outgoing message
+ initialsContainerLeftConstraint?.constant = 5
+ disclosureRightConstraint?.constant = -10
+ } else { // incoming message
+ initialsContainerLeftConstraint?.constant = 10
+ disclosureRightConstraint?.constant = -5
+ }
+ // setup colors
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
+ }
+ let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
+ nameLabel.textColor = textColor
+ disclosureImageView.tintColor = textColor
+ }
+
+ // MARK: Public
+
+ public enum ConstraintsID: String {
+ case initialsContainerLeftConstraint
+ case disclosureRightConstraint
+ }
+
+ /// The view container that holds contact initials
+ public lazy var initialsContainerView: UIView = {
+ let initialsContainer = UIView(frame: CGRect.zero)
+ initialsContainer.backgroundColor = .collectionViewBackground
+ return initialsContainer
+ }()
+
+ /// The label that display the contact initials
+ public lazy var initialsLabel: UILabel = {
+ let initialsLabel = UILabel(frame: CGRect.zero)
+ initialsLabel.textAlignment = .center
+ initialsLabel.textColor = .label
+ initialsLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
+ return initialsLabel
+ }()
+
+ /// The label that display contact name
+ public lazy var nameLabel: UILabel = {
+ let nameLabel = UILabel(frame: CGRect.zero)
+ nameLabel.numberOfLines = 0
+ return nameLabel
+ }()
+
+ /// The disclosure image view
+ public lazy var disclosureImageView: UIImageView = {
+ let disclosureImage = UIImage.messageKitImageWith(type: .disclosure)?.withRenderingMode(.alwaysTemplate)
+ let disclosure = UIImageView(image: disclosureImage)
+ return disclosure
+ }()
+}
diff --git a/Sources/Views/Cells/LinkPreviewMessageCell.swift b/Sources/Views/Cells/LinkPreviewMessageCell.swift
new file mode 100644
index 000000000..6d2df58f7
--- /dev/null
+++ b/Sources/Views/Cells/LinkPreviewMessageCell.swift
@@ -0,0 +1,111 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import UIKit
+
+open class LinkPreviewMessageCell: TextMessageCell {
+ // MARK: Open
+
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
+ linkPreviewView.titleLabel.font = attributes.linkPreviewFonts.titleFont
+ linkPreviewView.teaserLabel.font = attributes.linkPreviewFonts.teaserFont
+ linkPreviewView.domainLabel.font = attributes.linkPreviewFonts.domainFont
+ }
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ let displayDelegate = messagesCollectionView.messagesDisplayDelegate
+
+ if let textColor: UIColor = displayDelegate?.textColor(for: message, at: indexPath, in: messagesCollectionView) {
+ linkPreviewView.titleLabel.textColor = textColor
+ linkPreviewView.teaserLabel.textColor = textColor
+ linkPreviewView.domainLabel.textColor = textColor
+ }
+
+ guard case MessageKind.linkPreview(let linkItem) = message.kind else {
+ fatalError("LinkPreviewMessageCell received unhandled MessageDataType: \(message.kind)")
+ }
+
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ linkPreviewView.titleLabel.text = linkItem.title
+ linkPreviewView.teaserLabel.text = linkItem.teaser
+ linkPreviewView.domainLabel.text = linkItem.url.host?.lowercased()
+ linkPreviewView.imageView.image = linkItem.thumbnailImage
+ linkURL = linkItem.url
+
+ displayDelegate?.configureLinkPreviewImageView(
+ linkPreviewView.imageView,
+ for: message,
+ at: indexPath,
+ in: messagesCollectionView)
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ linkPreviewView.titleLabel.text = nil
+ linkPreviewView.teaserLabel.text = nil
+ linkPreviewView.domainLabel.text = nil
+ linkPreviewView.imageView.image = nil
+ linkURL = nil
+ }
+
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: linkPreviewView)
+
+ guard linkPreviewView.frame.contains(touchLocation), let url = linkURL else {
+ super.handleTapGesture(gesture)
+ return
+ }
+ delegate?.didSelectURL(url)
+ }
+
+ // MARK: Public
+
+ public lazy var linkPreviewView: LinkPreviewView = {
+ let view = LinkPreviewView()
+ view.translatesAutoresizingMaskIntoConstraints = false
+ messageContainerView.addSubview(view)
+
+ NSLayoutConstraint.activate([
+ view.leadingAnchor.constraint(
+ equalTo: messageContainerView.leadingAnchor,
+ constant: messageLabel.textInsets.left),
+ view.trailingAnchor.constraint(
+ equalTo: messageContainerView.trailingAnchor,
+ constant: messageLabel.textInsets.right * -1),
+ view.bottomAnchor.constraint(
+ equalTo: messageContainerView.bottomAnchor,
+ constant: messageLabel.textInsets.bottom * -1),
+ ])
+ return view
+ }()
+
+ // MARK: Private
+
+ private var linkURL: URL?
+}
diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift
index ed84ed752..44601b1b8 100644
--- a/Sources/Views/Cells/LocationMessageCell.swift
+++ b/Sources/Views/Cells/LocationMessageCell.swift
@@ -1,111 +1,126 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import MapKit
+import UIKit
/// A subclass of `MessageContentCell` used to display location messages.
open class LocationMessageCell: MessageContentCell {
-
- /// The activity indicator to be displayed while the map image is loading.
- open var activityIndicator = UIActivityIndicatorView(style: .gray)
-
- /// The image view holding the map image.
- open var imageView = UIImageView()
-
- private weak var snapShotter: MKMapSnapshotter?
-
- open override func setupSubviews() {
- super.setupSubviews()
- imageView.contentMode = .scaleAspectFill
- messageContainerView.addSubview(imageView)
- messageContainerView.addSubview(activityIndicator)
- setupConstraints()
+ // MARK: Open
+
+ /// The activity indicator to be displayed while the map image is loading.
+ open var activityIndicator = UIActivityIndicatorView(style: .medium)
+
+ /// The image view holding the map image.
+ open var imageView = UIImageView()
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ imageView.contentMode = .scaleAspectFill
+ messageContainerView.addSubview(imageView)
+ messageContainerView.addSubview(activityIndicator)
+ setupConstraints()
+ }
+
+ /// Responsible for setting up the constraints of the cell's subviews.
+ open func setupConstraints() {
+ imageView.fillSuperview()
+ activityIndicator.centerInSuperview()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ snapShotter?.cancel()
+ }
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ guard case .location(let locationItem) = message.kind else { fatalError("Configuring LocationMessageCell with wrong message kind") }
+ guard CLLocationCoordinate2DIsValid(locationItem.location.coordinate) else {
+ return
}
- /// Responsible for setting up the constraints of the cell's subviews.
- open func setupConstraints() {
- imageView.fillSuperview()
- activityIndicator.centerInSuperview()
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- snapShotter?.cancel()
+ let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView)
+ let annotationView = displayDelegate.annotationViewForLocation(
+ message: message,
+ at: indexPath,
+ in: messagesCollectionView)
+ let animationBlock = displayDelegate.animationBlockForLocation(
+ message: message,
+ at: indexPath,
+ in: messagesCollectionView)
+
+ activityIndicator.startAnimating()
+
+ let snapshotOptions = MKMapSnapshotter.Options()
+ snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span)
+ snapshotOptions.showsBuildings = options.showsBuildings
+ snapshotOptions.pointOfInterestFilter = options.showsPointsOfInterest ? .includingAll : .excludingAll
+
+ let snapShotter = MKMapSnapshotter(options: snapshotOptions)
+ self.snapShotter = snapShotter
+ snapShotter.start { snapshot, error in
+ defer {
+ self.activityIndicator.stopAnimating()
+ }
+ guard let snapshot = snapshot, error == nil else {
+ // show an error image?
+ return
+ }
+
+ guard let annotationView = annotationView else {
+ self.imageView.image = snapshot.image
+ return
+ }
+
+ UIGraphicsBeginImageContextWithOptions(snapshotOptions.size, true, 0)
+
+ snapshot.image.draw(at: .zero)
+
+ var point = snapshot.point(for: locationItem.location.coordinate)
+ // Move point to reflect annotation anchor
+ point.x -= annotationView.bounds.size.width / 2
+ point.y -= annotationView.bounds.size.height / 2
+ point.x += annotationView.centerOffset.x
+ point.y += annotationView.centerOffset.y
+
+ annotationView.image?.draw(at: point)
+ let composedImage = UIGraphicsGetImageFromCurrentImageContext()
+
+ UIGraphicsEndImageContext()
+ self.imageView.image = composedImage
+ animationBlock?(self.imageView)
}
+ }
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
- let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView)
- let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView)
- let animationBlock = displayDelegate.animationBlockForLocation(message: message, at: indexPath, in: messagesCollectionView)
-
- guard case let .location(locationItem) = message.kind else { fatalError("") }
-
- activityIndicator.startAnimating()
-
- let snapshotOptions = MKMapSnapshotter.Options()
- snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span)
- snapshotOptions.showsBuildings = options.showsBuildings
- snapshotOptions.showsPointsOfInterest = options.showsPointsOfInterest
-
- let snapShotter = MKMapSnapshotter(options: snapshotOptions)
- self.snapShotter = snapShotter
- snapShotter.start { (snapshot, error) in
- defer {
- self.activityIndicator.stopAnimating()
- }
- guard let snapshot = snapshot, error == nil else {
- //show an error image?
- return
- }
-
- guard let annotationView = annotationView else {
- self.imageView.image = snapshot.image
- return
- }
-
- UIGraphicsBeginImageContextWithOptions(snapshotOptions.size, true, 0)
-
- snapshot.image.draw(at: .zero)
-
- var point = snapshot.point(for: locationItem.location.coordinate)
- //Move point to reflect annotation anchor
- point.x -= annotationView.bounds.size.width / 2
- point.y -= annotationView.bounds.size.height / 2
- point.x += annotationView.centerOffset.x
- point.y += annotationView.centerOffset.y
-
- annotationView.image?.draw(at: point)
- let composedImage = UIGraphicsGetImageFromCurrentImageContext()
-
- UIGraphicsEndImageContext()
- self.imageView.image = composedImage
- animationBlock?(self.imageView)
- }
- }
+ // MARK: Private
+
+ private weak var snapShotter: MKMapSnapshotter?
}
diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift
index 94f885e80..f93779352 100644
--- a/Sources/Views/Cells/MediaMessageCell.swift
+++ b/Sources/Views/Cells/MediaMessageCell.swift
@@ -1,79 +1,96 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `MessageContentCell` used to display video and audio messages.
open class MediaMessageCell: MessageContentCell {
+ /// The play button view to display on video messages.
+ open lazy var playButtonView: PlayButtonView = {
+ let playButtonView = PlayButtonView()
+ return playButtonView
+ }()
- /// The play button view to display on video messages.
- open lazy var playButtonView: PlayButtonView = {
- let playButtonView = PlayButtonView()
- return playButtonView
- }()
+ /// The image view display the media content.
+ open var imageView: UIImageView = {
+ let imageView = UIImageView()
+ imageView.contentMode = .scaleAspectFill
+ return imageView
+ }()
- /// The image view display the media content.
- open var imageView: UIImageView = {
- let imageView = UIImageView()
- imageView.contentMode = .scaleAspectFill
- return imageView
- }()
+ // MARK: - Methods
- // MARK: - Methods
+ /// Responsible for setting up the constraints of the cell's subviews.
+ open func setupConstraints() {
+ imageView.fillSuperview()
+ playButtonView.centerInSuperview()
+ playButtonView.constraint(equalTo: CGSize(width: 35, height: 35))
+ }
- /// Responsible for setting up the constraints of the cell's subviews.
- open func setupConstraints() {
- imageView.fillSuperview()
- playButtonView.centerInSuperview()
- playButtonView.constraint(equalTo: CGSize(width: 35, height: 35))
- }
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(imageView)
+ messageContainerView.addSubview(playButtonView)
+ setupConstraints()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ imageView.image = nil
+ }
- open override func setupSubviews() {
- super.setupSubviews()
- messageContainerView.addSubview(imageView)
- messageContainerView.addSubview(playButtonView)
- setupConstraints()
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
+ switch message.kind {
+ case .photo(let mediaItem):
+ imageView.image = mediaItem.image ?? mediaItem.placeholderImage
+ playButtonView.isHidden = true
+ case .video(let mediaItem):
+ imageView.image = mediaItem.image ?? mediaItem.placeholderImage
+ playButtonView.isHidden = false
+ default:
+ break
+ }
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
+ displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView)
+ }
- switch message.kind {
- case .photo(let mediaItem):
- imageView.image = mediaItem.image ?? mediaItem.placeholderImage
- playButtonView.isHidden = true
- case .video(let mediaItem):
- imageView.image = mediaItem.image ?? mediaItem.placeholderImage
- playButtonView.isHidden = false
- default:
- break
- }
+ /// Handle tap gesture on contentView and its subviews.
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: imageView)
- displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView)
+ guard imageView.frame.contains(touchLocation) else {
+ super.handleTapGesture(gesture)
+ return
}
+ delegate?.didTapImage(in: self)
+ }
}
diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift
index 08114cb59..41cbd1f02 100644
--- a/Sources/Views/Cells/MessageCollectionViewCell.swift
+++ b/Sources/Views/Cells/MessageCollectionViewCell.swift
@@ -1,40 +1,45 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `UICollectionViewCell` to be used inside of a `MessagesCollectionView`.
open class MessageCollectionViewCell: UICollectionViewCell {
+ // MARK: Lifecycle
+
+ // MARK: - Initializers
- // MARK: - Initializers
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
- public override init(frame: CGRect) {
- super.init(frame: frame)
- }
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
+ // MARK: Open
+ /// Handle tap gesture on contentView and its subviews.
+ open func handleTapGesture(_: UIGestureRecognizer) {
+ // Should be overridden
+ }
}
diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift
index 3a194a68f..471f36c3b 100644
--- a/Sources/Views/Cells/MessageContentCell.swift
+++ b/Sources/Views/Cells/MessageContentCell.swift
@@ -1,301 +1,373 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
import UIKit
/// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages.
open class MessageContentCell: MessageCollectionViewCell {
-
- /// The image view displaying the avatar.
- open var avatarView = AvatarView()
-
- /// The container used for styling and holding the message's content view.
- open var messageContainerView: MessageContainerView = {
- let containerView = MessageContainerView()
- containerView.clipsToBounds = true
- containerView.layer.masksToBounds = true
- return containerView
- }()
-
- /// The top label of the cell.
- open var cellTopLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- label.textAlignment = .center
- return label
- }()
-
- /// The top label of the messageBubble.
- open var messageTopLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- return label
- }()
-
- /// The bottom label of the messageBubble.
- open var messageBottomLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- return label
- }()
-
- // Should only add customized subviews - don't change accessoryView itself.
- open var accessoryView: UIView = UIView()
-
- /// The `MessageCellDelegate` for the cell.
- open weak var delegate: MessageCellDelegate?
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- setupSubviews()
+ // MARK: Lifecycle
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ setupSubviews()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ setupSubviews()
+ }
+
+ // MARK: Open
+
+ /// The image view displaying the avatar.
+ open var avatarView = AvatarView()
+
+ /// The container used for styling and holding the message's content view.
+ open var messageContainerView: MessageContainerView = {
+ let containerView = MessageContainerView()
+ containerView.clipsToBounds = true
+ containerView.layer.masksToBounds = true
+ return containerView
+ }()
+
+ /// The top label of the cell.
+ open var cellTopLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ label.textAlignment = .center
+ return label
+ }()
+
+ /// The bottom label of the cell.
+ open var cellBottomLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ label.textAlignment = .center
+ return label
+ }()
+
+ /// The top label of the messageBubble.
+ open var messageTopLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ return label
+ }()
+
+ /// The bottom label of the messageBubble.
+ open var messageBottomLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ return label
+ }()
+
+ /// The time label of the messageBubble.
+ open var messageTimestampLabel = InsetLabel()
+
+ // Should only add customized subviews - don't change accessoryView itself.
+ open var accessoryView = UIView()
+
+ /// The `MessageCellDelegate` for the cell.
+ open weak var delegate: MessageCellDelegate?
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ cellTopLabel.text = nil
+ cellBottomLabel.text = nil
+ messageTopLabel.text = nil
+ messageBottomLabel.text = nil
+ messageTimestampLabel.attributedText = nil
+ }
+
+ open func setupSubviews() {
+ contentView.addSubviews(
+ accessoryView,
+ cellTopLabel,
+ messageTopLabel,
+ messageBottomLabel,
+ cellBottomLabel,
+ messageContainerView,
+ avatarView,
+ messageTimestampLabel)
+ }
+
+ // MARK: - Configuration
+
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
+ // Call this before other laying out other subviews
+ layoutMessageContainerView(with: attributes)
+ layoutMessageBottomLabel(with: attributes)
+ layoutCellBottomLabel(with: attributes)
+ layoutCellTopLabel(with: attributes)
+ layoutMessageTopLabel(with: attributes)
+ layoutAvatarView(with: attributes)
+ layoutAccessoryView(with: attributes)
+ layoutTimeLabelView(with: attributes)
+ }
+
+ /// Used to configure the cell.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` this cell displays.
+ /// - indexPath: The `IndexPath` for this cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained.
+ open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- setupSubviews()
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
- open func setupSubviews() {
- contentView.addSubview(accessoryView)
- contentView.addSubview(cellTopLabel)
- contentView.addSubview(messageTopLabel)
- contentView.addSubview(messageBottomLabel)
- contentView.addSubview(messageContainerView)
- contentView.addSubview(avatarView)
+ delegate = messagesCollectionView.messageCellDelegate
+
+ let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView)
+ let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView)
+
+ displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView)
+
+ displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView)
+
+ messageContainerView.backgroundColor = messageColor
+ messageContainerView.style = messageStyle
+
+ let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath)
+ let bottomCellLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath)
+ let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath)
+ let bottomMessageLabelText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath)
+ let messageTimestampLabelText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath)
+ cellTopLabel.attributedText = topCellLabelText
+ cellBottomLabel.attributedText = bottomCellLabelText
+ messageTopLabel.attributedText = topMessageLabelText
+ messageBottomLabel.attributedText = bottomMessageLabelText
+ messageTimestampLabel.attributedText = messageTimestampLabelText
+ messageTimestampLabel.isHidden = !messagesCollectionView.showMessageTimestampOnSwipeLeft
+ }
+
+ /// Handle tap gesture on contentView and its subviews.
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: self)
+
+ switch true {
+ case messageContainerView.frame
+ .contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)):
+ delegate?.didTapMessage(in: self)
+ case avatarView.frame.contains(touchLocation):
+ delegate?.didTapAvatar(in: self)
+ case cellTopLabel.frame.contains(touchLocation):
+ delegate?.didTapCellTopLabel(in: self)
+ case cellBottomLabel.frame.contains(touchLocation):
+ delegate?.didTapCellBottomLabel(in: self)
+ case messageTopLabel.frame.contains(touchLocation):
+ delegate?.didTapMessageTopLabel(in: self)
+ case messageBottomLabel.frame.contains(touchLocation):
+ delegate?.didTapMessageBottomLabel(in: self)
+ case accessoryView.frame.contains(touchLocation):
+ delegate?.didTapAccessoryView(in: self)
+ default:
+ delegate?.didTapBackground(in: self)
}
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- cellTopLabel.text = nil
- messageTopLabel.text = nil
- messageBottomLabel.text = nil
+ }
+
+ /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
+ open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ let touchPoint = gestureRecognizer.location(in: self)
+ guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
+ return messageContainerView.frame.contains(touchPoint)
+ }
+
+ /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
+ open func cellContentView(canHandle _: CGPoint) -> Bool {
+ false
+ }
+
+ // MARK: - Origin Calculations
+
+ /// Positions the cell's `AvatarView`.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ var origin: CGPoint = .zero
+ let padding = attributes.avatarLeadingTrailingPadding
+
+ switch attributes.avatarPosition.horizontal {
+ case .cellLeading:
+ origin.x = padding
+ case .cellTrailing:
+ origin.x = attributes.frame.width - attributes.avatarSize.width - padding
+ case .natural:
+ fatalError(MessageKitError.avatarPositionUnresolved)
}
- // MARK: - Configuration
-
- open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
- super.apply(layoutAttributes)
- guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
- // Call this before other laying out other subviews
- layoutMessageContainerView(with: attributes)
- layoutBottomLabel(with: attributes)
- layoutCellTopLabel(with: attributes)
- layoutMessageTopLabel(with: attributes)
- layoutAvatarView(with: attributes)
- layoutAccessoryView(with: attributes)
+ switch attributes.avatarPosition.vertical {
+ case .messageLabelTop:
+ origin.y = messageTopLabel.frame.minY
+ case .messageTop: // Needs messageContainerView frame to be set
+ origin.y = messageContainerView.frame.minY
+ case .messageBottom: // Needs messageContainerView frame to be set
+ origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height
+ case .messageCenter: // Needs messageContainerView frame to be set
+ origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height / 2)
+ case .cellBottom:
+ origin.y = attributes.frame.height - attributes.avatarSize.height
+ default:
+ break
}
- /// Used to configure the cell.
- ///
- /// - Parameters:
- /// - message: The `MessageType` this cell displays.
- /// - indexPath: The `IndexPath` for this cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained.
- open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
-
- delegate = messagesCollectionView.messageCellDelegate
-
- let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView)
- let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView)
-
- displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView)
-
- displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView)
-
- messageContainerView.backgroundColor = messageColor
- messageContainerView.style = messageStyle
-
- let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath)
- let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath)
- let bottomText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath)
-
- cellTopLabel.attributedText = topCellLabelText
- messageTopLabel.attributedText = topMessageLabelText
- messageBottomLabel.attributedText = bottomText
+ avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize)
+ }
+
+ /// Positions the cell's `MessageContainerView`.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ var origin: CGPoint = .zero
+
+ switch attributes.avatarPosition.vertical {
+ case .messageBottom:
+ origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.cellBottomLabelSize
+ .height - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes
+ .messageContainerPadding.top
+ case .messageCenter:
+ if attributes.avatarSize.height > attributes.messageContainerSize.height {
+ let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
+ origin.y = (attributes.size.height / 2) - (messageHeight / 2)
+ } else {
+ fallthrough
+ }
+ default:
+ if attributes.accessoryViewSize.height > attributes.messageContainerSize.height {
+ let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
+ origin.y = (attributes.size.height / 2) - (messageHeight / 2)
+ } else {
+ origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes
+ .messageContainerPadding.top
+ }
}
- /// Handle tap gesture on contentView and its subviews.
- open func handleTapGesture(_ gesture: UIGestureRecognizer) {
- let touchLocation = gesture.location(in: self)
-
- switch true {
- case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)):
- delegate?.didTapMessage(in: self)
- case avatarView.frame.contains(touchLocation):
- delegate?.didTapAvatar(in: self)
- case cellTopLabel.frame.contains(touchLocation):
- delegate?.didTapCellTopLabel(in: self)
- case messageTopLabel.frame.contains(touchLocation):
- delegate?.didTapMessageTopLabel(in: self)
- case messageBottomLabel.frame.contains(touchLocation):
- delegate?.didTapMessageBottomLabel(in: self)
- case accessoryView.frame.contains(touchLocation):
- delegate?.didTapAccessoryView(in: self)
- default:
- break
- }
+ let avatarPadding = attributes.avatarLeadingTrailingPadding
+ switch attributes.avatarPosition.horizontal {
+ case .cellLeading:
+ origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + avatarPadding
+ case .cellTrailing:
+ origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes
+ .messageContainerPadding.right - avatarPadding
+ case .natural:
+ fatalError(MessageKitError.avatarPositionUnresolved)
}
- /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
- open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
- let touchPoint = gestureRecognizer.location(in: self)
- guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
- return messageContainerView.frame.contains(touchPoint)
+ messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize)
+ }
+
+ /// Positions the cell's top label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutCellTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
+ cellTopLabel.textAlignment = attributes.cellTopLabelAlignment.textAlignment
+ cellTopLabel.textInsets = attributes.cellTopLabelAlignment.textInsets
+
+ cellTopLabel.frame = CGRect(origin: .zero, size: attributes.cellTopLabelSize)
+ }
+
+ /// Positions the cell's bottom label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutCellBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
+ cellBottomLabel.textAlignment = attributes.cellBottomLabelAlignment.textAlignment
+ cellBottomLabel.textInsets = attributes.cellBottomLabelAlignment.textInsets
+
+ let y = messageBottomLabel.frame.maxY
+ let origin = CGPoint(x: 0, y: y)
+
+ cellBottomLabel.frame = CGRect(origin: origin, size: attributes.cellBottomLabelSize)
+ }
+
+ /// Positions the message bubble's top label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
+ messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment
+ messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets
+
+ let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height
+ let origin = CGPoint(x: 0, y: y)
+
+ messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize)
+ }
+
+ /// Positions the message bubble's bottom label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutMessageBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
+ messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment
+ messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets
+
+ let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom
+ let origin = CGPoint(x: 0, y: y)
+
+ messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize)
+ }
+
+ /// Positions the cell's accessory view.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ var origin: CGPoint = .zero
+
+ // Accessory view is set at the side space of the messageContainerView
+ switch attributes.accessoryViewPosition {
+ case .messageLabelTop:
+ origin.y = messageTopLabel.frame.minY
+ case .messageTop:
+ origin.y = messageContainerView.frame.minY
+ case .messageBottom:
+ origin.y = messageContainerView.frame.maxY - attributes.accessoryViewSize.height
+ case .messageCenter:
+ origin.y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2)
+ case .cellBottom:
+ origin.y = attributes.frame.height - attributes.accessoryViewSize.height
+ default:
+ break
}
- /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
- open func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
- return false
+ // Accessory view is always on the opposite side of avatar
+ switch attributes.avatarPosition.horizontal {
+ case .cellLeading:
+ origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left
+ case .cellTrailing:
+ origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize
+ .width
+ case .natural:
+ fatalError(MessageKitError.avatarPositionUnresolved)
}
- // MARK: - Origin Calculations
-
- /// Positions the cell's `AvatarView`.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) {
- var origin: CGPoint = .zero
-
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- break
- case .cellTrailing:
- origin.x = attributes.frame.width - attributes.avatarSize.width
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
-
- switch attributes.avatarPosition.vertical {
- case .messageLabelTop:
- origin.y = messageTopLabel.frame.minY
- case .messageTop: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.minY
- case .messageBottom: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height
- case .messageCenter: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2)
- case .cellBottom:
- origin.y = attributes.frame.height - attributes.avatarSize.height
- default:
- break
- }
-
- avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize)
- }
-
- /// Positions the cell's `MessageContainerView`.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) {
- var origin: CGPoint = .zero
-
- switch attributes.avatarPosition.vertical {
- case .messageBottom:
- origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes.messageContainerPadding.top
- case .messageCenter:
- if attributes.avatarSize.height > attributes.messageContainerSize.height {
- let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
- origin.y = (attributes.size.height / 2) - (messageHeight / 2)
- } else {
- fallthrough
- }
- default:
- if attributes.accessoryViewSize.height > attributes.messageContainerSize.height {
- let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
- origin.y = (attributes.size.height / 2) - (messageHeight / 2)
- } else {
- origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top
- }
- }
-
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left
- case .cellTrailing:
- origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
-
- messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize)
- }
-
- /// Positions the cell's top label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutCellTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- cellTopLabel.frame = CGRect(origin: .zero, size: attributes.cellTopLabelSize)
- }
-
- /// Positions the message bubble's top label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment
- messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets
-
- let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height
- let origin = CGPoint(x: 0, y: y)
-
- messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize)
- }
-
- /// Positions the cell's bottom label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment
- messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets
-
- let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom
- let origin = CGPoint(x: 0, y: y)
-
- messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize)
- }
-
- /// Positions the cell's accessory view.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) {
-
- // Accessory view aligned to the middle of the messageContainerView
- let y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2)
-
- var origin = CGPoint(x: 0, y: y)
-
- // Accessory view is always on the opposite side of avatar
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left
- case .cellTrailing:
- origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize.width
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
-
- accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize)
- }
+ accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize)
+ }
+
+ /// Positions the message bubble's time label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutTimeLabelView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ let paddingLeft: CGFloat = 10
+ let origin = CGPoint(
+ x: self.frame.maxX + paddingLeft,
+ y: messageContainerView.frame.minY + messageContainerView.frame.height * 0.5 - messageTimestampLabel.font.ascender * 0.5)
+ let size = CGSize(width: attributes.messageTimeLabelSize.width, height: attributes.messageTimeLabelSize.height)
+ messageTimestampLabel.frame = CGRect(origin: origin, size: size)
+ }
}
diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift
index 481411ba3..08fcf381b 100644
--- a/Sources/Views/Cells/TextMessageCell.swift
+++ b/Sources/Views/Cells/TextMessageCell.swift
@@ -1,101 +1,102 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `MessageContentCell` used to display text messages.
open class TextMessageCell: MessageContentCell {
+ /// The label used to display the message's text.
+ open var messageLabel = MessageLabel()
- // MARK: - Properties
+ // MARK: - Properties
- /// The `MessageCellDelegate` for the cell.
- open override weak var delegate: MessageCellDelegate? {
- didSet {
- messageLabel.delegate = delegate
- }
+ /// The `MessageCellDelegate` for the cell.
+ open override weak var delegate: MessageCellDelegate? {
+ didSet {
+ messageLabel.delegate = delegate
}
+ }
- /// The label used to display the message's text.
- open var messageLabel = MessageLabel()
-
- // MARK: - Methods
+ // MARK: - Methods
- open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
- super.apply(layoutAttributes)
- if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes {
- messageLabel.textInsets = attributes.messageLabelInsets
- messageLabel.messageLabelFont = attributes.messageLabelFont
- messageLabel.frame = messageContainerView.bounds
- }
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes {
+ messageLabel.textInsets = attributes.messageLabelInsets
+ messageLabel.messageLabelFont = attributes.messageLabelFont
+ messageLabel.frame = messageContainerView.bounds
}
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- messageLabel.attributedText = nil
- messageLabel.text = nil
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ messageLabel.attributedText = nil
+ messageLabel.text = nil
+ }
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(messageLabel)
+ }
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
- open override func setupSubviews() {
- super.setupSubviews()
- messageContainerView.addSubview(messageLabel)
- }
-
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
-
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
+ let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView)
+
+ messageLabel.configure {
+ messageLabel.enabledDetectors = enabledDetectors
+ for detector in enabledDetectors {
+ let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath)
+ messageLabel.setAttributes(attributes, detector: detector)
+ }
+ let textMessageKind = message.kind.textMessageKind
+ switch textMessageKind {
+ case .text(let text), .emoji(let text):
+ let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
+ messageLabel.text = text
+ messageLabel.textColor = textColor
+ if let font = messageLabel.messageLabelFont {
+ messageLabel.font = font
}
-
- let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView)
-
- messageLabel.configure {
- messageLabel.enabledDetectors = enabledDetectors
- for detector in enabledDetectors {
- let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath)
- messageLabel.setAttributes(attributes, detector: detector)
- }
- switch message.kind {
- case .text(let text), .emoji(let text):
- let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
- messageLabel.text = text
- messageLabel.textColor = textColor
- if let font = messageLabel.messageLabelFont {
- messageLabel.font = font
- }
- case .attributedText(let text):
- messageLabel.attributedText = text
- default:
- break
- }
- }
- }
-
- /// Used to handle the cell's contentView's tap gesture.
- /// Return false when the contentView does not need to handle the gesture.
- open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
- return messageLabel.handleGesture(touchPoint)
+ case .attributedText(let text):
+ messageLabel.attributedText = text
+ default:
+ break
+ }
}
+ }
+ /// Used to handle the cell's contentView's tap gesture.
+ /// Return false when the contentView does not need to handle the gesture.
+ open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
+ messageLabel.handleGesture(touchPoint)
+ }
}
diff --git a/Sources/Views/Cells/TypingIndicatorCell.swift b/Sources/Views/Cells/TypingIndicatorCell.swift
new file mode 100644
index 000000000..368d25b50
--- /dev/null
+++ b/Sources/Views/Cells/TypingIndicatorCell.swift
@@ -0,0 +1,62 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import UIKit
+
+/// A subclass of `MessageCollectionViewCell` used to display the typing indicator.
+open class TypingIndicatorCell: MessageCollectionViewCell {
+ // MARK: Lifecycle
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupSubviews()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupSubviews()
+ }
+
+ // MARK: Open
+
+ open func setupSubviews() {
+ addSubview(typingBubble)
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ if typingBubble.isAnimating {
+ typingBubble.stopAnimating()
+ }
+ }
+
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+ typingBubble.frame = bounds.inset(by: insets)
+ }
+
+ // MARK: Public
+
+ public var insets = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0)
+
+ public let typingBubble = TypingBubble()
+}
diff --git a/Sources/Views/Headers & Footers/MessageReusableView.swift b/Sources/Views/Headers & Footers/MessageReusableView.swift
deleted file mode 100644
index 56270dd3c..000000000
--- a/Sources/Views/Headers & Footers/MessageReusableView.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import Foundation
-
-open class MessageReusableView: UICollectionReusableView {
-
- // MARK: - Initializers
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- }
-
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
-
-}
diff --git a/Sources/Views/HeadersFooters/MessageReusableView.swift b/Sources/Views/HeadersFooters/MessageReusableView.swift
new file mode 100644
index 000000000..77654ed72
--- /dev/null
+++ b/Sources/Views/HeadersFooters/MessageReusableView.swift
@@ -0,0 +1,36 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+open class MessageReusableView: UICollectionReusableView {
+ // MARK: - Initializers
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+}
diff --git a/Sources/Views/InsetLabel.swift b/Sources/Views/InsetLabel.swift
index 72bb9f8dc..9e6c7b62f 100644
--- a/Sources/Views/InsetLabel.swift
+++ b/Sources/Views/InsetLabel.swift
@@ -1,38 +1,34 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class InsetLabel: UILabel {
-
- open var textInsets: UIEdgeInsets = .zero {
- didSet { setNeedsDisplay() }
- }
-
- open override func drawText(in rect: CGRect) {
- let insetRect = rect.inset(by: textInsets)
- super.drawText(in: insetRect)
- }
-
+ open var textInsets: UIEdgeInsets = .zero {
+ didSet { setNeedsDisplay() }
+ }
+
+ open override func drawText(in rect: CGRect) {
+ let insetRect = rect.inset(by: textInsets)
+ super.drawText(in: insetRect)
+ }
}
diff --git a/Sources/Views/LinkPreviewView.swift b/Sources/Views/LinkPreviewView.swift
new file mode 100644
index 000000000..28fc1b1c1
--- /dev/null
+++ b/Sources/Views/LinkPreviewView.swift
@@ -0,0 +1,120 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import UIKit
+
+open class LinkPreviewView: UIView {
+ // MARK: Lifecycle
+
+ init() {
+ super.init(frame: .zero)
+
+ contentView.addSubview(titleLabel)
+ NSLayoutConstraint.activate([
+ titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
+ titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ ])
+
+ contentView.addSubview(teaserLabel)
+ NSLayoutConstraint.activate([
+ teaserLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ teaserLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3),
+ teaserLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ ])
+ teaserLabel.setContentHuggingPriority(.init(249), for: .vertical)
+
+ contentView.addSubview(domainLabel)
+ NSLayoutConstraint.activate([
+ domainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ domainLabel.topAnchor.constraint(equalTo: teaserLabel.bottomAnchor, constant: 3),
+ domainLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ domainLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
+ ])
+ }
+
+ required public init?(coder _: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: Internal
+
+ lazy var imageView: UIImageView = {
+ let imageView: UIImageView = .init()
+ imageView.clipsToBounds = true
+ imageView.contentMode = .scaleAspectFill
+ imageView.translatesAutoresizingMaskIntoConstraints = false
+
+ addSubview(imageView)
+
+ NSLayoutConstraint.activate([
+ imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ imageView.topAnchor.constraint(equalTo: topAnchor),
+ imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1),
+ imageView.widthAnchor.constraint(equalToConstant: LinkPreviewMessageSizeCalculator.imageViewSize),
+ imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
+ ])
+
+ return imageView
+ }()
+
+ lazy var titleLabel: UILabel = {
+ let label: UILabel = .init()
+ label.numberOfLines = 0
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ lazy var teaserLabel: UILabel = {
+ let label: UILabel = .init()
+ label.numberOfLines = 0
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ lazy var domainLabel: UILabel = {
+ let label: UILabel = .init()
+ label.numberOfLines = 0
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ // MARK: Private
+
+ private lazy var contentView: UIView = {
+ let view: UIView = .init(frame: .zero)
+ view.translatesAutoresizingMaskIntoConstraints = false
+
+ addSubview(view)
+
+ NSLayoutConstraint.activate([
+ view.topAnchor.constraint(equalTo: topAnchor),
+ view.leadingAnchor.constraint(
+ equalTo: imageView.trailingAnchor,
+ constant: LinkPreviewMessageSizeCalculator.imageViewMargin),
+ view.trailingAnchor.constraint(equalTo: trailingAnchor),
+ view.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+
+ return view
+ }()
+}
diff --git a/Sources/Views/MessageContainerView.swift b/Sources/Views/MessageContainerView.swift
index 714232936..e816f0d19 100644
--- a/Sources/Views/MessageContainerView.swift
+++ b/Sources/Views/MessageContainerView.swift
@@ -1,88 +1,89 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class MessageContainerView: UIImageView {
+ // MARK: Open
- // MARK: - Properties
-
- private let imageMask = UIImageView()
-
- open var style: MessageStyle = .none {
- didSet {
- applyMessageStyle()
- }
+ open var style: MessageStyle = .none {
+ didSet {
+ applyMessageStyle()
}
+ }
- open override var frame: CGRect {
- didSet {
- sizeMaskToView()
- }
+ open override var frame: CGRect {
+ didSet {
+ sizeMaskToView()
}
+ }
+
+ // MARK: Private
+
+ // MARK: - Properties
+
+ private let imageMask = UIImageView()
- // MARK: - Methods
+ // MARK: - Methods
- private func sizeMaskToView() {
- switch style {
- case .none, .custom:
- break
- case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline:
- imageMask.frame = bounds
- }
+ private func sizeMaskToView() {
+ switch style {
+ case .none, .custom:
+ break
+ case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline, .customImageTail:
+ imageMask.frame = bounds
}
+ }
- private func applyMessageStyle() {
- switch style {
- case .bubble, .bubbleTail:
- imageMask.image = style.image
- sizeMaskToView()
- mask = imageMask
- image = nil
- case .bubbleOutline(let color):
- let bubbleStyle: MessageStyle = .bubble
- imageMask.image = bubbleStyle.image
- sizeMaskToView()
- mask = imageMask
- image = style.image?.withRenderingMode(.alwaysTemplate)
- tintColor = color
- case .bubbleTailOutline(let color, let tail, let corner):
- let bubbleStyle: MessageStyle = .bubbleTail(tail, corner)
- imageMask.image = bubbleStyle.image
- sizeMaskToView()
- mask = imageMask
- image = style.image?.withRenderingMode(.alwaysTemplate)
- tintColor = color
- case .none:
- mask = nil
- image = nil
- tintColor = nil
- case .custom(let configurationClosure):
- mask = nil
- image = nil
- tintColor = nil
- configurationClosure(self)
- }
+ private func applyMessageStyle() {
+ switch style {
+ case .bubble, .bubbleTail, .customImageTail:
+ imageMask.image = style.image
+ sizeMaskToView()
+ mask = imageMask
+ image = nil
+ case .bubbleOutline(let color):
+ let bubbleStyle: MessageStyle = .bubble
+ imageMask.image = bubbleStyle.image
+ sizeMaskToView()
+ mask = imageMask
+ image = style.image?.withRenderingMode(.alwaysTemplate)
+ tintColor = color
+ case .bubbleTailOutline(let color, let tail, let corner):
+ let bubbleStyle: MessageStyle = .bubbleTail(tail, corner)
+ imageMask.image = bubbleStyle.image
+ sizeMaskToView()
+ mask = imageMask
+ image = style.image?.withRenderingMode(.alwaysTemplate)
+ tintColor = color
+ case .none:
+ mask = nil
+ image = nil
+ tintColor = nil
+ case .custom(let configurationClosure):
+ mask = nil
+ image = nil
+ tintColor = nil
+ configurationClosure(self)
}
+ }
}
diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift
index d451d4864..036bc3565 100644
--- a/Sources/Views/MessageLabel.swift
+++ b/Sources/Views/MessageLabel.swift
@@ -1,474 +1,581 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
+// MARK: - MessageLabel
+
open class MessageLabel: UILabel {
+ // MARK: Lifecycle
- // MARK: - Private Properties
-
- private lazy var layoutManager: NSLayoutManager = {
- let layoutManager = NSLayoutManager()
- layoutManager.addTextContainer(self.textContainer)
- return layoutManager
- }()
-
- private lazy var textContainer: NSTextContainer = {
- let textContainer = NSTextContainer()
- textContainer.lineFragmentPadding = 0
- textContainer.maximumNumberOfLines = self.numberOfLines
- textContainer.lineBreakMode = self.lineBreakMode
- textContainer.size = self.bounds.size
- return textContainer
- }()
-
- private lazy var textStorage: NSTextStorage = {
- let textStorage = NSTextStorage()
- textStorage.addLayoutManager(self.layoutManager)
- return textStorage
- }()
-
- private lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
-
- private var isConfiguring: Bool = false
+ // MARK: - Initializers
- // MARK: - Public Properties
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupView()
+ }
- open weak var delegate: MessageLabelDelegate?
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupView()
+ }
- open var enabledDetectors: [DetectorType] = [] {
- didSet {
- setTextStorage(attributedText, shouldParse: true)
- }
- }
+ // MARK: Open
- open override var attributedText: NSAttributedString? {
- didSet {
- setTextStorage(attributedText, shouldParse: true)
- }
+ // MARK: - Public Properties
+
+ open weak var delegate: MessageLabelDelegate?
+
+ open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+ open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+ open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+ open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+ open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+ open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+ open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+ open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:]
+
+ open var enabledDetectors: [DetectorType] = [] {
+ didSet {
+ setTextStorage(attributedText, shouldParse: true)
}
+ }
- open override var text: String? {
- didSet {
- setTextStorage(attributedText, shouldParse: true)
- }
+ open override var attributedText: NSAttributedString? {
+ didSet {
+ setTextStorage(attributedText, shouldParse: true)
}
+ }
- open override var font: UIFont! {
- didSet {
- setTextStorage(attributedText, shouldParse: false)
- }
+ open override var text: String? {
+ didSet {
+ setTextStorage(attributedText, shouldParse: true)
}
+ }
- open override var textColor: UIColor! {
- didSet {
- setTextStorage(attributedText, shouldParse: false)
- }
+ // swiftlint:disable:next implicitly_unwrapped_optional
+ open override var font: UIFont! {
+ didSet {
+ setTextStorage(attributedText, shouldParse: false)
}
+ }
- open override var lineBreakMode: NSLineBreakMode {
- didSet {
- textContainer.lineBreakMode = lineBreakMode
- if !isConfiguring { setNeedsDisplay() }
- }
+ // swiftlint:disable:next implicitly_unwrapped_optional
+ open override var textColor: UIColor! {
+ didSet {
+ setTextStorage(attributedText, shouldParse: false)
}
+ }
- open override var numberOfLines: Int {
- didSet {
- textContainer.maximumNumberOfLines = numberOfLines
- if !isConfiguring { setNeedsDisplay() }
- }
+ open override var lineBreakMode: NSLineBreakMode {
+ didSet {
+ textContainer.lineBreakMode = lineBreakMode
+ if !isConfiguring { setNeedsDisplay() }
}
+ }
- open override var textAlignment: NSTextAlignment {
- didSet {
- setTextStorage(attributedText, shouldParse: false)
- }
+ open override var numberOfLines: Int {
+ didSet {
+ textContainer.maximumNumberOfLines = numberOfLines
+ if !isConfiguring { setNeedsDisplay() }
}
+ }
- open var textInsets: UIEdgeInsets = .zero {
- didSet {
- if !isConfiguring { setNeedsDisplay() }
- }
+ open override var textAlignment: NSTextAlignment {
+ didSet {
+ setTextStorage(attributedText, shouldParse: false)
}
+ }
- open override var intrinsicContentSize: CGSize {
- var size = super.intrinsicContentSize
- size.width += textInsets.horizontal
- size.height += textInsets.vertical
- return size
+ open var textInsets: UIEdgeInsets = .zero {
+ didSet {
+ if !isConfiguring { setNeedsDisplay() }
}
-
- internal var messageLabelFont: UIFont?
+ }
- private var attributesNeedUpdate = false
+ open override var intrinsicContentSize: CGSize {
+ var size = super.intrinsicContentSize
+ size.width += textInsets.horizontal
+ size.height += textInsets.vertical
+ return size
+ }
- public static var defaultAttributes: [NSAttributedString.Key: Any] = {
- return [
- NSAttributedString.Key.foregroundColor: UIColor.darkText,
- NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
- NSAttributedString.Key.underlineColor: UIColor.darkText
- ]
- }()
+ // MARK: - Open Methods
- open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+ open override func drawText(in rect: CGRect) {
+ let insetRect = rect.inset(by: textInsets)
+ textContainer.size = CGSize(width: insetRect.width, height: insetRect.height)
- open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+ let origin = insetRect.origin
+ let range = layoutManager.glyphRange(for: textContainer)
- open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+ layoutManager.drawBackground(forGlyphRange: range, at: origin)
+ layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
+ }
- open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes
-
- open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes
-
- public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) {
- switch detector {
- case .phoneNumber:
- phoneNumberAttributes = attributes
- case .address:
- addressAttributes = attributes
- case .date:
- dateAttributes = attributes
- case .url:
- urlAttributes = attributes
- case .transitInformation:
- transitInformationAttributes = attributes
- }
- if isConfiguring {
- attributesNeedUpdate = true
- } else {
- updateAttributes(for: [detector])
+ open func handleGesture(_ touchLocation: CGPoint) -> Bool {
+ guard let index = stringIndex(at: touchLocation) else { return false }
+
+ for (detectorType, ranges) in rangesForDetectors {
+ for (range, value) in ranges {
+ if range.contains(index) {
+ handleGesture(for: detectorType, value: value)
+ return true
}
+ }
}
+ return false
+ }
+
+ // MARK: Public
+
+ public static var defaultAttributes: [NSAttributedString.Key: Any] = {
+ [
+ NSAttributedString.Key.foregroundColor: UIColor.darkText,
+ NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
+ NSAttributedString.Key.underlineColor: UIColor.darkText,
+ ]
+ }()
+
+ public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) {
+ switch detector {
+ case .phoneNumber:
+ phoneNumberAttributes = attributes
+ case .address:
+ addressAttributes = attributes
+ case .date:
+ dateAttributes = attributes
+ case .url:
+ urlAttributes = attributes
+ case .transitInformation:
+ transitInformationAttributes = attributes
+ case .mention:
+ mentionAttributes = attributes
+ case .hashtag:
+ hashtagAttributes = attributes
+ case .custom(let regex):
+ customAttributes[regex] = attributes
+ }
+ if isConfiguring {
+ attributesNeedUpdate = true
+ } else {
+ updateAttributes(for: [detector])
+ }
+ }
- // MARK: - Initializers
+ // MARK: - Public Methods
- public override init(frame: CGRect) {
- super.init(frame: frame)
- setupView()
+ public func configure(block: () -> Void) {
+ isConfiguring = true
+ block()
+ if attributesNeedUpdate {
+ updateAttributes(for: enabledDetectors)
}
+ attributesNeedUpdate = false
+ isConfiguring = false
+ setNeedsDisplay()
+ }
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setupView()
- }
+ // MARK: Internal
- // MARK: - Open Methods
+ internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
- open override func drawText(in rect: CGRect) {
+ internal var messageLabelFont: UIFont?
- let insetRect = rect.inset(by: textInsets)
- textContainer.size = CGSize(width: insetRect.width, height: rect.height)
+ // MARK: Private
- let origin = insetRect.origin
- let range = layoutManager.glyphRange(for: textContainer)
+ // MARK: - Private Properties
- layoutManager.drawBackground(forGlyphRange: range, at: origin)
- layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
- }
+ private lazy var layoutManager: NSLayoutManager = {
+ let layoutManager = NSLayoutManager()
+ layoutManager.addTextContainer(self.textContainer)
+ return layoutManager
+ }()
- // MARK: - Public Methods
-
- public func configure(block: () -> Void) {
- isConfiguring = true
- block()
- if attributesNeedUpdate {
- updateAttributes(for: enabledDetectors)
- }
- attributesNeedUpdate = false
- isConfiguring = false
- setNeedsDisplay()
- }
+ private lazy var textContainer: NSTextContainer = {
+ let textContainer = NSTextContainer()
+ textContainer.lineFragmentPadding = 0
+ textContainer.maximumNumberOfLines = self.numberOfLines
+ textContainer.lineBreakMode = self.lineBreakMode
+ textContainer.size = self.bounds.size
+ return textContainer
+ }()
- // MARK: - Private Methods
+ private lazy var textStorage: NSTextStorage = {
+ let textStorage = NSTextStorage()
+ textStorage.addLayoutManager(self.layoutManager)
+ return textStorage
+ }()
- private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) {
+ private var isConfiguring = false
- guard let newText = newText, newText.length > 0 else {
- textStorage.setAttributedString(NSAttributedString())
- setNeedsDisplay()
- return
- }
-
- let style = paragraphStyle(for: newText)
- let range = NSRange(location: 0, length: newText.length)
-
- let mutableText = NSMutableAttributedString(attributedString: newText)
- mutableText.addAttribute(.paragraphStyle, value: style, range: range)
-
- if shouldParse {
- rangesForDetectors.removeAll()
- let results = parse(text: mutableText)
- setRangesForDetectors(in: results)
- }
-
- for (detector, rangeTuples) in rangesForDetectors {
- if enabledDetectors.contains(detector) {
- let attributes = detectorAttributes(for: detector)
- rangeTuples.forEach { (range, _) in
- mutableText.addAttributes(attributes, range: range)
- }
- }
- }
+ private var attributesNeedUpdate = false
- let modifiedText = NSAttributedString(attributedString: mutableText)
- textStorage.setAttributedString(modifiedText)
+ // MARK: - Private Methods
- if !isConfiguring { setNeedsDisplay() }
+ private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) {
+ guard let newText = newText, newText.length > 0 else {
+ textStorage.setAttributedString(NSAttributedString())
+ setNeedsDisplay()
+ return
+ }
+
+ let style = paragraphStyle(for: newText)
+ let range = NSRange(location: 0, length: newText.length)
+
+ let mutableText = NSMutableAttributedString(attributedString: newText)
+ mutableText.addAttribute(.paragraphStyle, value: style, range: range)
+ if shouldParse {
+ rangesForDetectors.removeAll()
+ let results = parse(text: mutableText)
+ setRangesForDetectors(in: results)
}
-
- private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
- guard text.length > 0 else { return NSParagraphStyle() }
-
- var range = NSRange(location: 0, length: text.length)
- let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
- let style = existingStyle ?? NSMutableParagraphStyle()
-
- style.lineBreakMode = lineBreakMode
- style.alignment = textAlignment
-
- return style
+
+ for (detector, rangeTuples) in rangesForDetectors {
+ if enabledDetectors.contains(detector) {
+ let attributes = detectorAttributes(for: detector)
+ rangeTuples.forEach { range, _ in
+ mutableText.addAttributes(attributes, range: range)
+ }
+ }
}
- private func updateAttributes(for detectors: [DetectorType]) {
+ let modifiedText = NSAttributedString(attributedString: mutableText)
+ textStorage.setAttributedString(modifiedText)
+
+ if !isConfiguring { setNeedsDisplay() }
+ }
+
+ private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
+ guard text.length > 0 else { return NSParagraphStyle() }
- guard let attributedText = attributedText, attributedText.length > 0 else { return }
- let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
+ var range = NSRange(location: 0, length: text.length)
+ let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
+ let style = existingStyle ?? NSMutableParagraphStyle()
- for detector in detectors {
- guard let rangeTuples = rangesForDetectors[detector] else { continue }
+ style.lineBreakMode = lineBreakMode
+ style.alignment = textAlignment
- for (range, _) in rangeTuples {
- let attributes = detectorAttributes(for: detector)
- mutableAttributedString.addAttributes(attributes, range: range)
- }
+ return style
+ }
- let updatedString = NSAttributedString(attributedString: mutableAttributedString)
- textStorage.setAttributedString(updatedString)
+ private func updateAttributes(for detectors: [DetectorType]) {
+ guard let attributedText = attributedText, attributedText.length > 0 else { return }
+ let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
+
+ for detector in detectors {
+ guard let rangeTuples = rangesForDetectors[detector] else { continue }
+
+ for (range, _) in rangeTuples {
+ // This will enable us to attribute it with our own styles, since `UILabel` does not provide link attribute overrides like `UITextView` does
+ if detector.textCheckingType == .link {
+ mutableAttributedString.removeAttribute(NSAttributedString.Key.link, range: range)
}
+
+ let attributes = detectorAttributes(for: detector)
+ mutableAttributedString.addAttributes(attributes, range: range)
+ }
+
+ let updatedString = NSAttributedString(attributedString: mutableAttributedString)
+ textStorage.setAttributedString(updatedString)
+ }
+ }
+
+ private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] {
+ switch detectorType {
+ case .address:
+ return addressAttributes
+ case .date:
+ return dateAttributes
+ case .phoneNumber:
+ return phoneNumberAttributes
+ case .url:
+ return urlAttributes
+ case .transitInformation:
+ return transitInformationAttributes
+ case .mention:
+ return mentionAttributes
+ case .hashtag:
+ return hashtagAttributes
+ case .custom(let regex):
+ return customAttributes[regex] ?? MessageLabel.defaultAttributes
+ }
+ }
+
+ private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] {
+ switch checkingResultType {
+ case .address:
+ return addressAttributes
+ case .date:
+ return dateAttributes
+ case .phoneNumber:
+ return phoneNumberAttributes
+ case .link:
+ return urlAttributes
+ case .transitInformation:
+ return transitInformationAttributes
+ default:
+ fatalError(MessageKitError.unrecognizedCheckingResult)
+ }
+ }
+
+ private func setupView() {
+ numberOfLines = 0
+ lineBreakMode = .byWordWrapping
+ }
+
+ // MARK: - Parsing Text
+
+ private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
+ guard enabledDetectors.isEmpty == false else { return [] }
+ let range = NSRange(location: 0, length: text.length)
+ var matches = [NSTextCheckingResult]()
+
+ // Get matches of all .custom DetectorType and add it to matches array
+ let regexs = enabledDetectors
+ .filter { $0.isCustom }
+ .map { parseForMatches(with: $0, in: text, for: range) }
+ .joined()
+ matches.append(contentsOf: removeOverlappingResults(Array(regexs)))
+
+ // Get all Checking Types of detectors, except for .custom because they contain their own regex
+ let detectorCheckingTypes = enabledDetectors
+ .filter { !$0.isCustom }
+ .reduce(0) { $0 | $1.textCheckingType.rawValue }
+ if detectorCheckingTypes > 0, let detector = try? NSDataDetector(types: detectorCheckingTypes) {
+ let detectorMatches = detector.matches(in: text.string, options: [], range: range)
+ matches.append(contentsOf: detectorMatches)
}
- private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] {
-
- switch detectorType {
- case .address:
- return addressAttributes
- case .date:
- return dateAttributes
- case .phoneNumber:
- return phoneNumberAttributes
- case .url:
- return urlAttributes
- case .transitInformation:
- return transitInformationAttributes
- }
+ guard enabledDetectors.contains(.url) else {
+ return matches
+ }
+
+ // Enumerate NSAttributedString NSLinks and append ranges
+ var results: [NSTextCheckingResult] = matches
+ text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in
+ guard let url = value as? URL else { return }
+ let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url)
+ results.append(result)
}
- private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] {
- switch checkingResultType {
- case .address:
- return addressAttributes
- case .date:
- return dateAttributes
- case .phoneNumber:
- return phoneNumberAttributes
- case .link:
- return urlAttributes
- case .transitInformation:
- return transitInformationAttributes
- default:
- fatalError(MessageKitError.unrecognizedCheckingResult)
- }
+ return results
+ }
+
+ private func parseForMatches(
+ with detector: DetectorType,
+ in text: NSAttributedString,
+ for range: NSRange) -> [NSTextCheckingResult]
+ {
+ switch detector {
+ case .custom(let regex):
+ return regex.matches(in: text.string, options: [], range: range)
+ default:
+ fatalError("You must pass a .custom DetectorType")
}
+ }
+
+ private func removeOverlappingResults(_ results: [NSTextCheckingResult]) -> [NSTextCheckingResult]
+ {
+ var filteredResults: [NSTextCheckingResult] = []
- private func setupView() {
- numberOfLines = 0
- lineBreakMode = .byWordWrapping
+ for result in results {
+ let overlappingResults = results.filter { $0.range.intersection(result.range)?.length ?? 0 > 0 }
+
+ if overlappingResults.count <= 1 {
+ filteredResults.append(result)
+ continue
+ }
+
+ guard !filteredResults.contains(where: { $0.range == result.range }) else { continue }
+ let maxRangeResult = overlappingResults.max { $0.range.upperBound - $0.range.lowerBound < $1.range.upperBound - $1.range.lowerBound }
+ if let maxRangeResult {
+ filteredResults.append(maxRangeResult)
+ }
}
+
+ return filteredResults
+ }
+
+ private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
+ guard checkingResults.isEmpty == false else { return }
+
+ for result in checkingResults {
+ switch result.resultType {
+ case .address:
+ var ranges = rangesForDetectors[.address] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .address)
+ case .date:
+ var ranges = rangesForDetectors[.date] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .date)
+ case .phoneNumber:
+ var ranges = rangesForDetectors[.phoneNumber] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
+ case .link:
+ var ranges = rangesForDetectors[.url] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .url)
+ case .transitInformation:
+ var ranges = rangesForDetectors[.transitInformation] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
+ case .regularExpression:
+ guard
+ let text = text, let regex = result.regularExpression,
+ let range = Range(result.range, in: text) else { return }
+ let detector = DetectorType.custom(regex)
+ var ranges = rangesForDetectors[detector] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (
+ result.range,
+ .custom(pattern: regex.pattern, match: String(text[range])))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: detector)
+ default:
+ fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
+ }
+ }
+ }
- // MARK: - Parsing Text
-
- private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
- guard enabledDetectors.isEmpty == false else { return [] }
- let checkingTypes = enabledDetectors.reduce(0) { $0 | $1.textCheckingType.rawValue }
- let detector = try? NSDataDetector(types: checkingTypes)
- let range = NSRange(location: 0, length: text.length)
- let matches = detector?.matches(in: text.string, options: [], range: range) ?? []
+ // MARK: - Gesture Handling
- guard enabledDetectors.contains(.url) else {
- return matches
- }
+ private func stringIndex(at location: CGPoint) -> Int? {
+ guard textStorage.length > 0 else { return nil }
- // Enumerate NSAttributedString NSLinks and append ranges
- var results: [NSTextCheckingResult] = matches
+ var location = location
- text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in
- guard let url = value as? URL else { return }
- let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url)
- results.append(result)
- }
+ location.x -= textInsets.left
+ location.y -= textInsets.top
- return results
- }
+ let index = layoutManager.glyphIndex(for: location, in: textContainer)
- private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
-
- guard checkingResults.isEmpty == false else { return }
-
- for result in checkingResults {
-
- switch result.resultType {
- case .address:
- var ranges = rangesForDetectors[.address] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .address)
- case .date:
- var ranges = rangesForDetectors[.date] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .date)
- case .phoneNumber:
- var ranges = rangesForDetectors[.phoneNumber] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
- case .link:
- var ranges = rangesForDetectors[.url] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .url)
- case .transitInformation:
- var ranges = rangesForDetectors[.transitInformation] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
-
- default:
- fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
- }
+ let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)
- }
+ var characterIndex: Int?
+ if lineRect.contains(location) {
+ characterIndex = layoutManager.characterIndexForGlyph(at: index)
}
- // MARK: - Gesture Handling
+ return characterIndex
+ }
+
+ // swiftlint:disable cyclomatic_complexity
+ private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) {
+ switch value {
+ case .addressComponents(let addressComponents):
+ var transformedAddressComponents = [String: String]()
+ guard let addressComponents = addressComponents else { return }
+ addressComponents.forEach { key, value in
+ transformedAddressComponents[key.rawValue] = value
+ }
+ handleAddress(transformedAddressComponents)
+ case .phoneNumber(let phoneNumber):
+ guard let phoneNumber = phoneNumber else { return }
+ handlePhoneNumber(phoneNumber)
+ case .date(let date):
+ guard let date = date else { return }
+ handleDate(date)
+ case .link(let url):
+ guard let url = url else { return }
+ handleURL(url)
+ case .transitInfoComponents(let transitInformation):
+ var transformedTransitInformation = [String: String]()
+ guard let transitInformation = transitInformation else { return }
+ transitInformation.forEach { key, value in
+ transformedTransitInformation[key.rawValue] = value
+ }
+ handleTransitInformation(transformedTransitInformation)
+ case .custom(let pattern, let match):
+ guard let match = match else { return }
+ switch detectorType {
+ case .hashtag:
+ handleHashtag(match)
+ case .mention:
+ handleMention(match)
+ default:
+ handleCustom(pattern, match: match)
+ }
+ }
+ }
- private func stringIndex(at location: CGPoint) -> Int? {
- guard textStorage.length > 0 else { return nil }
+ // swiftlint:enable cyclomatic_complexity
- var location = location
+ private func handleAddress(_ addressComponents: [String: String]) {
+ delegate?.didSelectAddress(addressComponents)
+ }
- location.x -= textInsets.left
- location.y -= textInsets.top
+ private func handleDate(_ date: Date) {
+ delegate?.didSelectDate(date)
+ }
- let index = layoutManager.glyphIndex(for: location, in: textContainer)
+ private func handleURL(_ url: URL) {
+ delegate?.didSelectURL(url)
+ }
- let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)
-
- var characterIndex: Int?
-
- if lineRect.contains(location) {
- characterIndex = layoutManager.characterIndexForGlyph(at: index)
- }
-
- return characterIndex
+ private func handlePhoneNumber(_ phoneNumber: String) {
+ delegate?.didSelectPhoneNumber(phoneNumber)
+ }
- }
+ private func handleTransitInformation(_ components: [String: String]) {
+ delegate?.didSelectTransitInformation(components)
+ }
- open func handleGesture(_ touchLocation: CGPoint) -> Bool {
+ private func handleHashtag(_ hashtag: String) {
+ delegate?.didSelectHashtag(hashtag)
+ }
- guard let index = stringIndex(at: touchLocation) else { return false }
+ private func handleMention(_ mention: String) {
+ delegate?.didSelectMention(mention)
+ }
- for (detectorType, ranges) in rangesForDetectors {
- for (range, value) in ranges {
- if range.contains(index) {
- handleGesture(for: detectorType, value: value)
- return true
- }
- }
- }
- return false
- }
-
- private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) {
-
- switch value {
- case let .addressComponents(addressComponents):
- var transformedAddressComponents = [String: String]()
- guard let addressComponents = addressComponents else { return }
- addressComponents.forEach { (key, value) in
- transformedAddressComponents[key.rawValue] = value
- }
- handleAddress(transformedAddressComponents)
- case let .phoneNumber(phoneNumber):
- guard let phoneNumber = phoneNumber else { return }
- handlePhoneNumber(phoneNumber)
- case let .date(date):
- guard let date = date else { return }
- handleDate(date)
- case let .link(url):
- guard let url = url else { return }
- handleURL(url)
- case let .transitInfoComponents(transitInformation):
- var transformedTransitInformation = [String: String]()
- guard let transitInformation = transitInformation else { return }
- transitInformation.forEach { (key, value) in
- transformedTransitInformation[key.rawValue] = value
- }
- handleTransitInformation(transformedTransitInformation)
- }
- }
-
- private func handleAddress(_ addressComponents: [String: String]) {
- delegate?.didSelectAddress(addressComponents)
- }
-
- private func handleDate(_ date: Date) {
- delegate?.didSelectDate(date)
- }
-
- private func handleURL(_ url: URL) {
- delegate?.didSelectURL(url)
- }
-
- private func handlePhoneNumber(_ phoneNumber: String) {
- delegate?.didSelectPhoneNumber(phoneNumber)
- }
-
- private func handleTransitInformation(_ components: [String: String]) {
- delegate?.didSelectTransitInformation(components)
- }
-
+ private func handleCustom(_ pattern: String, match: String) {
+ delegate?.didSelectCustom(pattern, match: match)
+ }
}
-private enum MessageTextCheckingType {
- case addressComponents([NSTextCheckingKey: String]?)
- case date(Date?)
- case phoneNumber(String?)
- case link(URL?)
- case transitInfoComponents([NSTextCheckingKey: String]?)
+// MARK: - MessageTextCheckingType
+
+internal enum MessageTextCheckingType {
+ case addressComponents([NSTextCheckingKey: String]?)
+ case date(Date?)
+ case phoneNumber(String?)
+ case link(URL?)
+ case transitInfoComponents([NSTextCheckingKey: String]?)
+ case custom(pattern: String, match: String?)
}
diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift
index ee1e4cd3c..a551ef2f9 100644
--- a/Sources/Views/MessagesCollectionView.swift
+++ b/Sources/Views/MessagesCollectionView.swift
@@ -1,159 +1,225 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
import UIKit
open class MessagesCollectionView: UICollectionView {
+ // MARK: Lifecycle
- // MARK: - Properties
+ // MARK: - Initializers
- open weak var messagesDataSource: MessagesDataSource?
+ public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
+ super.init(frame: frame, collectionViewLayout: layout)
+ backgroundColor = .collectionViewBackground
+ registerReusableViews()
+ setupGestureRecognizers()
+ }
- open weak var messagesDisplayDelegate: MessagesDisplayDelegate?
+ required public init?(coder _: NSCoder) {
+ super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
+ }
- open weak var messagesLayoutDelegate: MessagesLayoutDelegate?
+ public convenience init() {
+ self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
+ }
- open weak var messageCellDelegate: MessageCellDelegate?
+ // MARK: Open
- private var indexPathForLastItem: IndexPath? {
- let lastSection = numberOfSections - 1
- guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil }
- return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection)
- }
+ // MARK: - Properties
- // MARK: - Initializers
+ open weak var messagesDataSource: MessagesDataSource?
- public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
- super.init(frame: frame, collectionViewLayout: layout)
- backgroundColor = .white
- registerReusableViews()
- setupGestureRecognizers()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
- }
+ open weak var messagesDisplayDelegate: MessagesDisplayDelegate?
- public convenience init() {
- self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
- }
+ open weak var messagesLayoutDelegate: MessagesLayoutDelegate?
+
+ open weak var messageCellDelegate: MessageCellDelegate?
- // MARK: - Methods
-
- private func registerReusableViews() {
- register(TextMessageCell.self)
- register(MediaMessageCell.self)
- register(LocationMessageCell.self)
- register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader)
- register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter)
+ open var isTypingIndicatorHidden: Bool {
+ messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden
+ }
+
+ open var messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout {
+ guard let layout = collectionViewLayout as? MessagesCollectionViewFlowLayout else {
+ fatalError(MessageKitError.layoutUsedOnForeignType)
+ }
+ return layout
+ }
+
+ @objc
+ open func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ guard gesture.state == .ended else { return }
+
+ let touchLocation = gesture.location(in: self)
+ guard let indexPath = indexPathForItem(at: touchLocation) else { return }
+
+ let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell
+ cell?.handleTapGesture(gesture)
+ }
+
+ // MARK: Public
+
+ // NOTE: It's possible for small content size this wouldn't work - https://github.com/MessageKit/MessageKit/issues/725
+ public func scrollToLastItem(at pos: UICollectionView.ScrollPosition = .bottom, animated: Bool = true) {
+ guard let indexPath = indexPathForLastItem else { return }
+
+ scrollToItem(at: indexPath, at: pos, animated: animated)
+ }
+
+ public func reloadDataAndKeepOffset() {
+ // stop scrolling
+ setContentOffset(contentOffset, animated: false)
+
+ // calculate the offset and reloadData
+ let beforeContentSize = contentSize
+ reloadData()
+ layoutIfNeeded()
+ let afterContentSize = contentSize
+
+ // reset the contentOffset after data is updated
+ let newOffset = CGPoint(
+ x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
+ y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
+ setContentOffset(newOffset, animated: false)
+ }
+
+ /// A method that by default checks if the section is the last in the
+ /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
+ /// is FALSE
+ ///
+ /// - Parameter section
+ /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
+ public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
+ messagesCollectionViewFlowLayout.isSectionReservedForTypingIndicator(section)
+ }
+
+ // MARK: - View Register/Dequeue
+
+ /// Registers a particular cell using its reuse-identifier
+ public func register(_ cellClass: T.Type) {
+ register(cellClass, forCellWithReuseIdentifier: String(describing: T.self))
+ }
+
+ /// Registers a reusable view for a specific SectionKind
+ public func register(_ reusableViewClass: T.Type, forSupplementaryViewOfKind kind: String) {
+ register(
+ reusableViewClass,
+ forSupplementaryViewOfKind: kind,
+ withReuseIdentifier: String(describing: T.self))
+ }
+
+ /// Registers a nib with reusable view for a specific SectionKind
+ public func register(
+ _ nib: UINib? = UINib(nibName: String(describing: T.self), bundle: nil),
+ headerFooterClassOfNib _: T.Type,
+ forSupplementaryViewOfKind kind: String)
+ {
+ register(
+ nib,
+ forSupplementaryViewOfKind: kind,
+ withReuseIdentifier: String(describing: T.self))
+ }
+
+ /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
+ public func dequeueReusableCell