├── .gitignore ├── .travis.yml ├── Examples └── Example1 │ ├── Example1.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── Example1.xcworkspace │ └── contents.xcworkspacedata │ ├── Example1 │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ChatViewController.h │ ├── ChatViewController.m │ ├── ChatsListViewController.h │ ├── ChatsListViewController.m │ ├── Info.plist │ ├── Message.h │ ├── Message.m │ ├── Person.h │ ├── Person.m │ ├── PersonCellNode.h │ ├── PersonCellNode.m │ └── main.m │ ├── Podfile │ └── Podfile.lock ├── LICENSE ├── MXRMessenger.podspec ├── MXRMessenger ├── Core │ ├── MXRGrowingEditableTextNode.h │ ├── MXRGrowingEditableTextNode.m │ ├── MXRMessenger.h │ ├── MXRMessengerMedium.h │ ├── UIBezierPath+MXRMessenger.h │ ├── UIBezierPath+MXRMessenger.m │ ├── UIColor+MXRMessenger.h │ ├── UIColor+MXRMessenger.m │ ├── UIImage+MXRMessenger.h │ └── UIImage+MXRMessenger.m ├── MessageCell │ ├── MXRMessageCell.h │ ├── MXRMessageCellConstants.h │ ├── MXRMessageCellFactory.h │ ├── MXRMessageCellFactory.m │ ├── MXRMessageCellNode.h │ ├── MXRMessageCellNode.m │ ├── MXRMessageContentNode+Subclasses.h │ ├── MXRMessageContentNode.h │ ├── MXRMessageContentNode.m │ ├── MXRMessageContentNodeDelegate.h │ ├── MXRMessageDateFormatter.h │ ├── MXRMessageDateFormatter.m │ ├── MXRMessageImageNode.h │ ├── MXRMessageImageNode.m │ ├── MXRMessageMediaCollectionNode.h │ ├── MXRMessageMediaCollectionNode.m │ ├── MXRMessageMediumCellNode.h │ ├── MXRMessageMediumCellNode.m │ ├── MXRMessageNodeConfiguration.h │ ├── MXRMessageNodeConfiguration.m │ ├── MXRMessageTextNode.h │ ├── MXRMessageTextNode.m │ ├── MXRPlayButtonNode.h │ └── MXRPlayButtonNode.m └── ViewController │ ├── MXRMessengerInputToolbar.h │ ├── MXRMessengerInputToolbar.m │ ├── MXRMessengerNode.h │ ├── MXRMessengerNode.m │ ├── MXRMessengerViewController.h │ ├── MXRMessengerViewController.m │ ├── _MXRMessengerInputToolbarContainerView.h │ └── _MXRMessengerInputToolbarContainerView.m ├── README.md └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | Pods/ 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -workspace Example/MXRMessenger.xcworkspace -scheme MXRMessenger-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Examples/Example1/Example1.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1C0CB8A267CF899E2FBE36A6 /* Pods_Example1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 948132184711D5622FAF932B /* Pods_Example1.framework */; }; 11 | 76D0518A1EB731CE0051A6BC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76D051871EB731CE0051A6BC /* AppDelegate.m */; }; 12 | 76D0518B1EB731CE0051A6BC /* ChatViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 76D051891EB731CE0051A6BC /* ChatViewController.m */; }; 13 | 76D0518E1EB7345A0051A6BC /* Message.m in Sources */ = {isa = PBXBuildFile; fileRef = 76D0518D1EB7345A0051A6BC /* Message.m */; }; 14 | 76D051911EB74DC70051A6BC /* ChatsListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 76D051901EB74DC70051A6BC /* ChatsListViewController.m */; }; 15 | 76D051941EB74DF20051A6BC /* Person.m in Sources */ = {isa = PBXBuildFile; fileRef = 76D051931EB74DF20051A6BC /* Person.m */; }; 16 | 76D051971EB753C70051A6BC /* PersonCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 76D051961EB753C70051A6BC /* PersonCellNode.m */; }; 17 | 76FB32F81EB730B000E206A4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 76FB32F71EB730B000E206A4 /* main.m */; }; 18 | 76FB33031EB730B000E206A4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 76FB33021EB730B000E206A4 /* Assets.xcassets */; }; 19 | 76FB33061EB730B000E206A4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 76FB33041EB730B000E206A4 /* LaunchScreen.storyboard */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 76D051861EB731CE0051A6BC /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 24 | 76D051871EB731CE0051A6BC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 25 | 76D051881EB731CE0051A6BC /* ChatViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatViewController.h; sourceTree = ""; }; 26 | 76D051891EB731CE0051A6BC /* ChatViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatViewController.m; sourceTree = ""; }; 27 | 76D0518C1EB7345A0051A6BC /* Message.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Message.h; sourceTree = ""; }; 28 | 76D0518D1EB7345A0051A6BC /* Message.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Message.m; sourceTree = ""; }; 29 | 76D0518F1EB74DC70051A6BC /* ChatsListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatsListViewController.h; sourceTree = ""; }; 30 | 76D051901EB74DC70051A6BC /* ChatsListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatsListViewController.m; sourceTree = ""; }; 31 | 76D051921EB74DF20051A6BC /* Person.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Person.h; sourceTree = ""; }; 32 | 76D051931EB74DF20051A6BC /* Person.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Person.m; sourceTree = ""; }; 33 | 76D051951EB753C70051A6BC /* PersonCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PersonCellNode.h; sourceTree = ""; }; 34 | 76D051961EB753C70051A6BC /* PersonCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PersonCellNode.m; sourceTree = ""; }; 35 | 76FB32F31EB730B000E206A4 /* Example1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example1.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 76FB32F71EB730B000E206A4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 37 | 76FB33021EB730B000E206A4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 76FB33051EB730B000E206A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | 76FB33071EB730B000E206A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 948132184711D5622FAF932B /* Pods_Example1.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example1.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | E6CAA01A5334E18052E68020 /* Pods-Example1.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example1.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Example1/Pods-Example1.debug.xcconfig"; sourceTree = ""; }; 42 | F457E5C59D63865C31BC1F87 /* Pods-Example1.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example1.release.xcconfig"; path = "Pods/Target Support Files/Pods-Example1/Pods-Example1.release.xcconfig"; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 76FB32F01EB730B000E206A4 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | 1C0CB8A267CF899E2FBE36A6 /* Pods_Example1.framework in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 1FF236D54FB3CCEFCA5370C7 /* Pods */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | E6CAA01A5334E18052E68020 /* Pods-Example1.debug.xcconfig */, 61 | F457E5C59D63865C31BC1F87 /* Pods-Example1.release.xcconfig */, 62 | ); 63 | name = Pods; 64 | sourceTree = ""; 65 | }; 66 | 76FB32EA1EB730B000E206A4 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 76FB32F51EB730B000E206A4 /* Example1 */, 70 | 76FB32F41EB730B000E206A4 /* Products */, 71 | 1FF236D54FB3CCEFCA5370C7 /* Pods */, 72 | D7FBAD7661321E8519A3B28E /* Frameworks */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 76FB32F41EB730B000E206A4 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 76FB32F31EB730B000E206A4 /* Example1.app */, 80 | ); 81 | name = Products; 82 | sourceTree = ""; 83 | }; 84 | 76FB32F51EB730B000E206A4 /* Example1 */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 76D051861EB731CE0051A6BC /* AppDelegate.h */, 88 | 76D051871EB731CE0051A6BC /* AppDelegate.m */, 89 | 76D0518F1EB74DC70051A6BC /* ChatsListViewController.h */, 90 | 76D051901EB74DC70051A6BC /* ChatsListViewController.m */, 91 | 76D051881EB731CE0051A6BC /* ChatViewController.h */, 92 | 76D051891EB731CE0051A6BC /* ChatViewController.m */, 93 | 76D0518C1EB7345A0051A6BC /* Message.h */, 94 | 76D0518D1EB7345A0051A6BC /* Message.m */, 95 | 76D051921EB74DF20051A6BC /* Person.h */, 96 | 76D051931EB74DF20051A6BC /* Person.m */, 97 | 76D051951EB753C70051A6BC /* PersonCellNode.h */, 98 | 76D051961EB753C70051A6BC /* PersonCellNode.m */, 99 | 76FB33021EB730B000E206A4 /* Assets.xcassets */, 100 | 76FB33041EB730B000E206A4 /* LaunchScreen.storyboard */, 101 | 76FB33071EB730B000E206A4 /* Info.plist */, 102 | 76FB32F61EB730B000E206A4 /* Supporting Files */, 103 | ); 104 | path = Example1; 105 | sourceTree = ""; 106 | }; 107 | 76FB32F61EB730B000E206A4 /* Supporting Files */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 76FB32F71EB730B000E206A4 /* main.m */, 111 | ); 112 | name = "Supporting Files"; 113 | sourceTree = ""; 114 | }; 115 | D7FBAD7661321E8519A3B28E /* Frameworks */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 948132184711D5622FAF932B /* Pods_Example1.framework */, 119 | ); 120 | name = Frameworks; 121 | sourceTree = ""; 122 | }; 123 | /* End PBXGroup section */ 124 | 125 | /* Begin PBXNativeTarget section */ 126 | 76FB32F21EB730B000E206A4 /* Example1 */ = { 127 | isa = PBXNativeTarget; 128 | buildConfigurationList = 76FB330A1EB730B000E206A4 /* Build configuration list for PBXNativeTarget "Example1" */; 129 | buildPhases = ( 130 | 3F9A75F7ED1E970738AD027C /* [CP] Check Pods Manifest.lock */, 131 | 76FB32EF1EB730B000E206A4 /* Sources */, 132 | 76FB32F01EB730B000E206A4 /* Frameworks */, 133 | 76FB32F11EB730B000E206A4 /* Resources */, 134 | D578B37414322394AB8F1871 /* [CP] Embed Pods Frameworks */, 135 | 50544C6C433D8D83D3C35068 /* [CP] Copy Pods Resources */, 136 | ); 137 | buildRules = ( 138 | ); 139 | dependencies = ( 140 | ); 141 | name = Example1; 142 | productName = Example1; 143 | productReference = 76FB32F31EB730B000E206A4 /* Example1.app */; 144 | productType = "com.apple.product-type.application"; 145 | }; 146 | /* End PBXNativeTarget section */ 147 | 148 | /* Begin PBXProject section */ 149 | 76FB32EB1EB730B000E206A4 /* Project object */ = { 150 | isa = PBXProject; 151 | attributes = { 152 | LastUpgradeCheck = 0810; 153 | ORGANIZATIONNAME = "Scott Kensell"; 154 | TargetAttributes = { 155 | 76FB32F21EB730B000E206A4 = { 156 | CreatedOnToolsVersion = 8.1; 157 | ProvisioningStyle = Automatic; 158 | }; 159 | }; 160 | }; 161 | buildConfigurationList = 76FB32EE1EB730B000E206A4 /* Build configuration list for PBXProject "Example1" */; 162 | compatibilityVersion = "Xcode 3.2"; 163 | developmentRegion = English; 164 | hasScannedForEncodings = 0; 165 | knownRegions = ( 166 | en, 167 | Base, 168 | ); 169 | mainGroup = 76FB32EA1EB730B000E206A4; 170 | productRefGroup = 76FB32F41EB730B000E206A4 /* Products */; 171 | projectDirPath = ""; 172 | projectRoot = ""; 173 | targets = ( 174 | 76FB32F21EB730B000E206A4 /* Example1 */, 175 | ); 176 | }; 177 | /* End PBXProject section */ 178 | 179 | /* Begin PBXResourcesBuildPhase section */ 180 | 76FB32F11EB730B000E206A4 /* Resources */ = { 181 | isa = PBXResourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | 76FB33061EB730B000E206A4 /* LaunchScreen.storyboard in Resources */, 185 | 76FB33031EB730B000E206A4 /* Assets.xcassets in Resources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXResourcesBuildPhase section */ 190 | 191 | /* Begin PBXShellScriptBuildPhase section */ 192 | 3F9A75F7ED1E970738AD027C /* [CP] Check Pods Manifest.lock */ = { 193 | isa = PBXShellScriptBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | ); 197 | inputPaths = ( 198 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 199 | "${PODS_ROOT}/Manifest.lock", 200 | ); 201 | name = "[CP] Check Pods Manifest.lock"; 202 | outputPaths = ( 203 | "$(DERIVED_FILE_DIR)/Pods-Example1-checkManifestLockResult.txt", 204 | ); 205 | runOnlyForDeploymentPostprocessing = 0; 206 | shellPath = /bin/sh; 207 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 208 | showEnvVarsInLog = 0; 209 | }; 210 | 50544C6C433D8D83D3C35068 /* [CP] Copy Pods Resources */ = { 211 | isa = PBXShellScriptBuildPhase; 212 | buildActionMask = 2147483647; 213 | files = ( 214 | ); 215 | inputPaths = ( 216 | ); 217 | name = "[CP] Copy Pods Resources"; 218 | outputPaths = ( 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | shellPath = /bin/sh; 222 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Example1/Pods-Example1-resources.sh\"\n"; 223 | showEnvVarsInLog = 0; 224 | }; 225 | D578B37414322394AB8F1871 /* [CP] Embed Pods Frameworks */ = { 226 | isa = PBXShellScriptBuildPhase; 227 | buildActionMask = 2147483647; 228 | files = ( 229 | ); 230 | inputPaths = ( 231 | "${SRCROOT}/Pods/Target Support Files/Pods-Example1/Pods-Example1-frameworks.sh", 232 | "${BUILT_PRODUCTS_DIR}/MXRMessenger/MXRMessenger.framework", 233 | "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", 234 | "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", 235 | "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", 236 | "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", 237 | ); 238 | name = "[CP] Embed Pods Frameworks"; 239 | outputPaths = ( 240 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MXRMessenger.framework", 241 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", 242 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", 243 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", 244 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | shellPath = /bin/sh; 248 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Example1/Pods-Example1-frameworks.sh\"\n"; 249 | showEnvVarsInLog = 0; 250 | }; 251 | /* End PBXShellScriptBuildPhase section */ 252 | 253 | /* Begin PBXSourcesBuildPhase section */ 254 | 76FB32EF1EB730B000E206A4 /* Sources */ = { 255 | isa = PBXSourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | 76D051941EB74DF20051A6BC /* Person.m in Sources */, 259 | 76D0518A1EB731CE0051A6BC /* AppDelegate.m in Sources */, 260 | 76D0518E1EB7345A0051A6BC /* Message.m in Sources */, 261 | 76D051971EB753C70051A6BC /* PersonCellNode.m in Sources */, 262 | 76FB32F81EB730B000E206A4 /* main.m in Sources */, 263 | 76D051911EB74DC70051A6BC /* ChatsListViewController.m in Sources */, 264 | 76D0518B1EB731CE0051A6BC /* ChatViewController.m in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXSourcesBuildPhase section */ 269 | 270 | /* Begin PBXVariantGroup section */ 271 | 76FB33041EB730B000E206A4 /* LaunchScreen.storyboard */ = { 272 | isa = PBXVariantGroup; 273 | children = ( 274 | 76FB33051EB730B000E206A4 /* Base */, 275 | ); 276 | name = LaunchScreen.storyboard; 277 | sourceTree = ""; 278 | }; 279 | /* End PBXVariantGroup section */ 280 | 281 | /* Begin XCBuildConfiguration section */ 282 | 76FB33081EB730B000E206A4 /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ALWAYS_SEARCH_USER_PATHS = NO; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_WARN_BOOL_CONVERSION = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 295 | CLANG_WARN_EMPTY_BODY = YES; 296 | CLANG_WARN_ENUM_CONVERSION = YES; 297 | CLANG_WARN_INFINITE_RECURSION = YES; 298 | CLANG_WARN_INT_CONVERSION = YES; 299 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 300 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 301 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 302 | CLANG_WARN_UNREACHABLE_CODE = YES; 303 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 304 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 305 | COPY_PHASE_STRIP = NO; 306 | DEBUG_INFORMATION_FORMAT = dwarf; 307 | ENABLE_STRICT_OBJC_MSGSEND = YES; 308 | ENABLE_TESTABILITY = YES; 309 | GCC_C_LANGUAGE_STANDARD = gnu99; 310 | GCC_DYNAMIC_NO_PIC = NO; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_OPTIMIZATION_LEVEL = 0; 313 | GCC_PREPROCESSOR_DEFINITIONS = ( 314 | "DEBUG=1", 315 | "$(inherited)", 316 | ); 317 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 318 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 319 | GCC_WARN_UNDECLARED_SELECTOR = YES; 320 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 321 | GCC_WARN_UNUSED_FUNCTION = YES; 322 | GCC_WARN_UNUSED_VARIABLE = YES; 323 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 324 | MTL_ENABLE_DEBUG_INFO = YES; 325 | ONLY_ACTIVE_ARCH = YES; 326 | SDKROOT = iphoneos; 327 | }; 328 | name = Debug; 329 | }; 330 | 76FB33091EB730B000E206A4 /* Release */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_ANALYZER_NONNULL = YES; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_WARN_BOOL_CONVERSION = YES; 340 | CLANG_WARN_CONSTANT_CONVERSION = YES; 341 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 342 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 343 | CLANG_WARN_EMPTY_BODY = YES; 344 | CLANG_WARN_ENUM_CONVERSION = YES; 345 | CLANG_WARN_INFINITE_RECURSION = YES; 346 | CLANG_WARN_INT_CONVERSION = YES; 347 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 348 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 349 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 350 | CLANG_WARN_UNREACHABLE_CODE = YES; 351 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 352 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 353 | COPY_PHASE_STRIP = NO; 354 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 355 | ENABLE_NS_ASSERTIONS = NO; 356 | ENABLE_STRICT_OBJC_MSGSEND = YES; 357 | GCC_C_LANGUAGE_STANDARD = gnu99; 358 | GCC_NO_COMMON_BLOCKS = YES; 359 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 360 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 361 | GCC_WARN_UNDECLARED_SELECTOR = YES; 362 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 363 | GCC_WARN_UNUSED_FUNCTION = YES; 364 | GCC_WARN_UNUSED_VARIABLE = YES; 365 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 366 | MTL_ENABLE_DEBUG_INFO = NO; 367 | SDKROOT = iphoneos; 368 | VALIDATE_PRODUCT = YES; 369 | }; 370 | name = Release; 371 | }; 372 | 76FB330B1EB730B000E206A4 /* Debug */ = { 373 | isa = XCBuildConfiguration; 374 | baseConfigurationReference = E6CAA01A5334E18052E68020 /* Pods-Example1.debug.xcconfig */; 375 | buildSettings = { 376 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 377 | INFOPLIST_FILE = Example1/Info.plist; 378 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 379 | PRODUCT_BUNDLE_IDENTIFIER = edu.ScottKensell.Example1; 380 | PRODUCT_NAME = "$(TARGET_NAME)"; 381 | }; 382 | name = Debug; 383 | }; 384 | 76FB330C1EB730B000E206A4 /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | baseConfigurationReference = F457E5C59D63865C31BC1F87 /* Pods-Example1.release.xcconfig */; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | INFOPLIST_FILE = Example1/Info.plist; 390 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 391 | PRODUCT_BUNDLE_IDENTIFIER = edu.ScottKensell.Example1; 392 | PRODUCT_NAME = "$(TARGET_NAME)"; 393 | }; 394 | name = Release; 395 | }; 396 | /* End XCBuildConfiguration section */ 397 | 398 | /* Begin XCConfigurationList section */ 399 | 76FB32EE1EB730B000E206A4 /* Build configuration list for PBXProject "Example1" */ = { 400 | isa = XCConfigurationList; 401 | buildConfigurations = ( 402 | 76FB33081EB730B000E206A4 /* Debug */, 403 | 76FB33091EB730B000E206A4 /* Release */, 404 | ); 405 | defaultConfigurationIsVisible = 0; 406 | defaultConfigurationName = Release; 407 | }; 408 | 76FB330A1EB730B000E206A4 /* Build configuration list for PBXNativeTarget "Example1" */ = { 409 | isa = XCConfigurationList; 410 | buildConfigurations = ( 411 | 76FB330B1EB730B000E206A4 /* Debug */, 412 | 76FB330C1EB730B000E206A4 /* Release */, 413 | ); 414 | defaultConfigurationIsVisible = 0; 415 | defaultConfigurationName = Release; 416 | }; 417 | /* End XCConfigurationList section */ 418 | }; 419 | rootObject = 76FB32EB1EB730B000E206A4 /* Project object */; 420 | } 421 | -------------------------------------------------------------------------------- /Examples/Example1/Example1.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Example1/Example1.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRAppDelegate.h 3 | // MXRMessenger 4 | // 5 | // Created by Scott Kensell on 04/18/2017. 6 | // Copyright (c) 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRAppDelegate.m 3 | // MXRMessenger 4 | // 5 | // Created by Scott Kensell on 04/18/2017. 6 | // Copyright (c) 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | #import "ChatsListViewController.h" 12 | 13 | @implementation AppDelegate 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 16 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 17 | UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController:[ChatsListViewController new]]; 18 | nav.navigationBar.translucent = NO; 19 | self.window.rootViewController = nav; 20 | self.window.backgroundColor = [UIColor whiteColor]; 21 | [self.window makeKeyAndVisible]; 22 | return YES; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Examples/Example1/Example1/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/ChatViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRExampleViewController.h 3 | // MXRMessenger 4 | // 5 | // Created by Scott Kensell on 4/18/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class Person; 12 | 13 | @interface ChatViewController : MXRMessengerViewController 14 | 15 | - (instancetype)initWithPerson:(Person*)person; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/ChatViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRExampleViewController.m 3 | // MXRMessenger 4 | // 5 | // Created by Scott Kensell on 4/18/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import "ChatViewController.h" 10 | 11 | #import 12 | #import 13 | 14 | #import "Message.h" 15 | #import "Person.h" 16 | 17 | @interface ChatViewController () 18 | 19 | @property (nonatomic, strong) MXRMessageCellFactory* cellFactory; 20 | @property (nonatomic, strong) NSMutableArray * messages; 21 | @property (nonatomic, strong) NSURL* otherPersonsAvatar; 22 | 23 | @end 24 | 25 | @implementation ChatViewController 26 | 27 | - (instancetype)initWithPerson:(Person *)person { 28 | self = [super init]; 29 | if (self) { 30 | self.title = person.name; 31 | 32 | MXRMessengerIconButtonNode* addPhotosBarButtonButtonNode = [MXRMessengerIconButtonNode buttonWithIcon:[[MXRMessengerPlusIconNode alloc] init] matchingToolbar:self.toolbar]; 33 | [addPhotosBarButtonButtonNode addTarget:self action:@selector(tapAddPhotos:) forControlEvents:ASControlNodeEventTouchUpInside]; 34 | self.toolbar.leftButtonsNode = addPhotosBarButtonButtonNode; 35 | [self.toolbar.defaultSendButton addTarget:self action:@selector(tapSend:) forControlEvents:ASControlNodeEventTouchUpInside]; 36 | 37 | _otherPersonsAvatar = person.avatarURL; 38 | } 39 | return self; 40 | } 41 | 42 | - (void)viewDidLoad { 43 | [super viewDidLoad]; 44 | self.navigationItem.title = self.title; 45 | 46 | self.node.tableNode.delegate = self; // actually redundant bc MXRMessenger sets it 47 | self.node.tableNode.dataSource = self; 48 | self.node.tableNode.allowsSelection = YES; 49 | 50 | [self customizeCellFactory]; 51 | [self fetchMessages]; 52 | } 53 | 54 | - (void)customizeCellFactory { 55 | MXRMessageCellLayoutConfiguration* layoutConfigForMe = [MXRMessageCellLayoutConfiguration rightToLeft]; 56 | MXRMessageCellLayoutConfiguration* layoutConfigForOthers = [MXRMessageCellLayoutConfiguration leftToRight]; 57 | 58 | MXRMessageAvatarConfiguration* avatarConfigForMe = nil; 59 | MXRMessageAvatarConfiguration* avatarConfigForOthers = [[MXRMessageAvatarConfiguration alloc] init]; 60 | 61 | MXRMessageTextConfiguration* textConfigForMe = [[MXRMessageTextConfiguration alloc] initWithFont:nil textColor:[UIColor whiteColor] backgroundColor:[UIColor mxr_fbMessengerBlue]]; 62 | textConfigForMe.linkHighlightStyle = ASTextNodeHighlightStyleDark; 63 | MXRMessageTextConfiguration* textConfigForOthers = [[MXRMessageTextConfiguration alloc] initWithFont:nil textColor:[UIColor blackColor] backgroundColor:[UIColor mxr_bubbleLightGrayColor]]; 64 | textConfigForOthers.linkHighlightStyle = ASTextNodeHighlightStyleLight; 65 | CGFloat maxCornerRadius = textConfigForMe.maxCornerRadius; 66 | 67 | MXRMessageImageConfiguration* imageConfig = [[MXRMessageImageConfiguration alloc] init]; 68 | imageConfig.maxCornerRadius = maxCornerRadius; 69 | MXRMessageMediaCollectionConfiguration* mediaCollectionConfig = [[MXRMessageMediaCollectionConfiguration alloc] init]; 70 | mediaCollectionConfig.maxCornerRadius = maxCornerRadius; 71 | 72 | textConfigForMe.menuItemTypes |= MXRMessageMenuItemTypeDelete; 73 | textConfigForOthers.menuItemTypes |= MXRMessageMenuItemTypeDelete; 74 | imageConfig.menuItemTypes |= MXRMessageMenuItemTypeDelete; 75 | imageConfig.showsUIMenuControllerOnLongTap = YES; 76 | CGFloat s = [UIScreen mainScreen].scale; 77 | imageConfig.borderWidth = s > 0 ? (1.0f/s) : 0.5f; 78 | 79 | MXRMessageCellConfiguration* cellConfigForMe = [[MXRMessageCellConfiguration alloc] initWithLayoutConfig:layoutConfigForMe avatarConfig:avatarConfigForMe textConfig:textConfigForMe imageConfig:imageConfig mediaCollectionConfig:mediaCollectionConfig]; 80 | MXRMessageCellConfiguration* cellConfigForOthers = [[MXRMessageCellConfiguration alloc] initWithLayoutConfig:layoutConfigForOthers avatarConfig:avatarConfigForOthers textConfig:textConfigForOthers imageConfig:imageConfig mediaCollectionConfig:mediaCollectionConfig]; 81 | 82 | self.cellFactory = [[MXRMessageCellFactory alloc] initWithCellConfigForMe:cellConfigForMe cellConfigForOthers:cellConfigForOthers]; 83 | self.cellFactory.dataSource = self; 84 | self.cellFactory.contentNodeDelegate = self; 85 | self.cellFactory.mediaCollectionDelegate = self; 86 | } 87 | 88 | - (void)viewWillAppear:(BOOL)animated { 89 | [super viewWillAppear:animated]; 90 | [self.navigationController setNavigationBarHidden:NO animated:animated]; 91 | } 92 | 93 | #pragma mark - Target-Action 94 | 95 | - (void)tapAddPhotos:(id)sender { 96 | UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Photos!" message:@"You should present a photo picker here." preferredStyle:UIAlertControllerStyleAlert]; 97 | [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {}]]; 98 | [self presentViewController:alert animated:YES completion:nil]; 99 | } 100 | 101 | - (void)tapSend:(id)sender { 102 | NSString* text = [self.toolbar clearText]; 103 | if (text.length == 0) return; 104 | Message* message = [[Message alloc] init]; 105 | message.text = text; 106 | message.senderID = 0; 107 | message.timestamp = [NSDate date].timeIntervalSince1970; 108 | [self.messages insertObject:message atIndex:0]; 109 | [self.cellFactory updateTableNode:self.node.tableNode animated:YES withInsertions:@[[NSIndexPath indexPathForRow:0 inSection:0]] deletions:nil reloads:nil completion:nil]; 110 | } 111 | 112 | #pragma mark - MXMessageCellFactoryDataSource 113 | 114 | - (BOOL)cellFactory:(MXRMessageCellFactory *)cellFactory isMessageFromMeAtRow:(NSInteger)row { 115 | return self.messages[row].senderID == 0; 116 | } 117 | 118 | - (NSURL *)cellFactory:(MXRMessageCellFactory *)cellFactory avatarURLAtRow:(NSInteger)row { 119 | return [self cellFactory:cellFactory isMessageFromMeAtRow:row] ? nil : self.otherPersonsAvatar; 120 | } 121 | 122 | - (NSTimeInterval)cellFactory:(MXRMessageCellFactory *)cellFactory timeIntervalSince1970AtRow:(NSInteger)row { 123 | return self.messages[row].timestamp; 124 | } 125 | 126 | #pragma mark - ASTable 127 | 128 | - (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section { return self.messages.count; } 129 | 130 | - (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { 131 | Message* message = self.messages[indexPath.row]; 132 | if (message.media.count > 1) { 133 | return [self.cellFactory cellNodeBlockWithMedia:message.media tableNode:tableNode row:indexPath.row]; 134 | } else if (message.media.count == 1) { 135 | MessageMedium* medium = message.media.firstObject; 136 | return [self.cellFactory cellNodeBlockWithImageURL:medium.photoURL showsPlayButton:(medium.videoURL != nil) tableNode:tableNode row:indexPath.row]; 137 | } else { 138 | return [self.cellFactory cellNodeBlockWithText:message.text tableNode:tableNode row:indexPath.row]; 139 | } 140 | } 141 | 142 | - (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 143 | [self dismissKeyboard]; 144 | [tableNode deselectRowAtIndexPath:indexPath animated:NO]; 145 | } 146 | 147 | #pragma mark - MXRMessageContentNodeDelegate 148 | 149 | - (void)messageContentNode:(MXRMessageContentNode *)node didTapMenuItemWithType:(MXRMessageMenuItemTypes)menuItemType { 150 | if (menuItemType == MXRMessageMenuItemTypeDelete) { 151 | ASDisplayNode* supernode = [node supernode]; 152 | if ([supernode isKindOfClass:[MXRMessageCellNode class]]) { 153 | [self deleteCellNode:(MXRMessageCellNode*)supernode]; 154 | } 155 | } 156 | } 157 | 158 | - (void)messageContentNode:(MXRMessageContentNode *)node didSingleTap:(id)sender { 159 | if (![node.supernode isKindOfClass:[MXRMessageCellNode class]]) return; 160 | MXRMessageCellNode* cellNode = (MXRMessageCellNode*)node.supernode; 161 | if ([node isKindOfClass:[MXRMessageImageNode class]]) { 162 | // present a media viewer 163 | NSLog(@"Single tapped an image"); 164 | return; 165 | } else if ([node isKindOfClass:[MXRMessageTextNode class]]) { 166 | NSLog(@"Single tapped text"); 167 | [self.cellFactory toggleDateHeaderNodeVisibilityForCellNode:cellNode]; 168 | } 169 | } 170 | 171 | - (void)messageContentNode:(MXRMessageContentNode*)node didTapURL:(NSURL*)url { 172 | NSLog(@"Tapped URL"); 173 | } 174 | 175 | - (void)messageContentNode:(MXRMessageContentNode*)node didLongTapURL:(NSURL*)url { 176 | NSLog(@"Long-Tapped URL"); 177 | } 178 | 179 | #pragma mark - MXMessageMediaCollectionNodeDelegate 180 | 181 | - (void)messageMediaCollectionNode:(MXRMessageMediaCollectionNode *)messageMediaCollectionNode didSelectMedium:(id)medium atIndexPath:(NSIndexPath *)indexPath { 182 | // Show a media viewer 183 | } 184 | 185 | #pragma mark - Helper 186 | 187 | - (void)deleteCellNode:(ASCellNode*)cellNode { 188 | NSIndexPath* indexPath = [cellNode indexPath]; 189 | if (!indexPath) return; 190 | // delete cell in model 191 | } 192 | 193 | - (void)fetchMessages { 194 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 195 | NSMutableArray* messages = [[NSMutableArray alloc] init]; 196 | NSTimeInterval timestamp = [NSDate date].timeIntervalSince1970 - 1800; 197 | for (int i = 0; i < 20; i++) { 198 | Message* m = [Message randomMessage]; 199 | m.timestamp = timestamp; 200 | timestamp -= arc4random_uniform(1200); 201 | [messages addObject:m]; 202 | } 203 | dispatch_async(dispatch_get_main_queue(), ^{ 204 | self.messages = messages; 205 | [self.node.tableNode reloadData]; 206 | }); 207 | }); 208 | 209 | } 210 | 211 | @end 212 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/ChatsListViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChatsListViewController.h 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ChatsListViewController : ASViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/ChatsListViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ChatsListViewController.m 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import "ChatsListViewController.h" 10 | 11 | #import "Person.h" 12 | #import "PersonCellNode.h" 13 | #import "ChatViewController.h" 14 | 15 | @interface ChatsListViewController () 16 | 17 | @property (nonatomic, strong) NSArray* people; 18 | 19 | @end 20 | 21 | @implementation ChatsListViewController 22 | 23 | - (instancetype)init { 24 | self = [super initWithNode:[[ASTableNode alloc] init]]; 25 | if (self) { 26 | self.title = @"Messages"; 27 | self.people = [Person someRandomPeople]; 28 | } 29 | return self; 30 | } 31 | 32 | - (void)viewDidLoad { 33 | [super viewDidLoad]; 34 | self.node.dataSource = self; 35 | self.node.delegate = self; 36 | self.navigationController.navigationBarHidden = NO; 37 | } 38 | 39 | #pragma mark - ASTable 40 | 41 | - (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section { 42 | return self.people.count; 43 | } 44 | 45 | - (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { 46 | Person* person = self.people[indexPath.row]; 47 | return ^{ return [[PersonCellNode alloc] initWithPerson:person]; }; 48 | } 49 | 50 | - (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 51 | Person* person = self.people[indexPath.row]; 52 | ChatViewController* vc = [[ChatViewController alloc] initWithPerson:person]; 53 | [self.navigationController pushViewController:vc animated:YES]; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | NSAppTransportSecurity 22 | 23 | NSAllowsArbitraryLoads 24 | 25 | 26 | LSRequiresIPhoneOS 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/Message.h: -------------------------------------------------------------------------------- 1 | // 2 | // Message.h 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | @class MessageMedium; 14 | 15 | @interface Message : NSObject 16 | 17 | @property (nonatomic, assign) NSInteger senderID; 18 | @property (nonatomic, assign) NSTimeInterval timestamp; 19 | @property (nonatomic, strong) NSString* text; 20 | @property (nonatomic, strong) NSArray* media; 21 | 22 | + (instancetype)randomMessage; 23 | 24 | @end 25 | 26 | @interface MessageMedium : NSObject 27 | 28 | @property (nonatomic, strong) NSURL* photoURL; 29 | @property (nonatomic, strong) NSURL* videoURL; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/Message.m: -------------------------------------------------------------------------------- 1 | // 2 | // Message.m 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import "Message.h" 10 | 11 | #import 12 | 13 | @implementation Message 14 | 15 | + (instancetype)randomMessage { 16 | static dispatch_once_t onceToken; 17 | static NSArray* texts = nil; 18 | static NSArray* linkTexts = nil; 19 | static NSArray* photoCategories = nil; 20 | dispatch_once(&onceToken, ^{ 21 | texts = @[@"You ever have the feeling that you're not sure if you're awake or still dreaming?", 22 | @"All the time. It's called mescaline and it is the only way to fly.", 23 | @"It sounds to me like you need to unplug, man. A little R&R. What do you think, Dujour, should we take him with us?", 24 | @"Definitely.", 25 | @"Frankly, my dear, I don't give a damn.", 26 | @"I'm gonna make him an offer he can't refuse.", 27 | @"Here's looking at you, kid.", 28 | @"May the Force be with you.", 29 | @"Bond. James Bond.", 30 | @"Show me the money!", 31 | @"My mama always said life was like a box of chocolates. You never know what you're gonna get.", 32 | @"Are you sure this line is clean?", 33 | @"Yeah, 'course I'm sure.", 34 | @"I'd better go.", 35 | @"Lieutenant...", 36 | @"You got the money?", 37 | @"Two grand.", 38 | @"Something wrong, man? You look a little whiter than usual.", 39 | @"My computer....it..you ever have that feeling where you don't know if you're awake or still dreaming?", 40 | @"Trinity..._The_ Trinity? The one the cracked the IRS d-base?", 41 | @"Please, just listen. I know why you're here, Neo. I know what you've been doing. I know why you hardly sleep, and why night after night you sit at your computer. You're looking for him. I know, because I was once looking for the same thing. And when he found me, he told me I wasn't really looking for him, I was looking for an answer. It's the question that drives us, Neo. It's the question that brought you here. You know the question, just as I did...", 42 | @"What is the Matrix?", 43 | @"The answer is out there, Neo. It's looking for you...and it will find you...if you want it to....", 44 | @"You have a problem with authority, Mr. Anderson.", 45 | @"Yeah, that's me.", 46 | @"Hello, Neo. Do you know who this is?", 47 | @"Morpheus?", 48 | @"Yes...I've been looking for you, Neo. I don't know if you're ready to see what I want to show you, but unfortunately you and I have run out of time. They're coming for you, Neo, and I don't know what they're going to do.", 49 | ]; 50 | linkTexts = @[@"Hey, here's that link to google: http://www.google.com. It's just the search engine.", 51 | @"Here's a long link to Mount hood: https://www.google.com/maps/place/Mt+Hood/@45.3736131,-121.704706,15z/data=!4m5!3m4!1s0x54be1c5501719a05:0x831d76c0b7aea9ea!8m2!3d45.373615!4d-121.6959511 and a little text after it.", 52 | @"facebook.com", 53 | @"www.facebook.com", 54 | @"https://www.nytimes.com/2017/12/06/upshot/what-happened-to-the-american-boomtown.html?hp&action=click&pgtype=Homepage&clickSource=story-heading&module=second-column-region®ion=top-news&WT.nav=top-news is a nytimes article. You should check it out.", 55 | @"Check out that NYTimes article again: https://www.nytimes.com/2017/12/06/upshot/what-happened-to-the-american-boomtown.html?hp&action=click&pgtype=Homepage&clickSource=story-heading&module=second-column-region®ion=top-news&WT.nav=top-news", 56 | @"Check out facebook.com and http://www.google.com and https://www.instagram.com"]; 57 | photoCategories = @[@"abstract", @"city", @"people", @"transport", @"animals", @"food", @"nature", @"business", @"nightlife", @"sports", @"cats", @"fashion", @"technics"]; 58 | }); 59 | Message* m = [[Message alloc] init]; 60 | m.senderID = arc4random_uniform(2); 61 | if (arc4random_uniform(100) < 10) { 62 | NSUInteger numberOfMedia = arc4random_uniform(100) < 50 ? 1 : (arc4random_uniform(9) + 1); 63 | NSMutableArray* media = [[NSMutableArray alloc] init]; 64 | for (int i = 0; i < numberOfMedia; i++) { 65 | MessageMedium* medium = [[MessageMedium alloc] init]; 66 | medium.photoURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://lorempixel.com/%@/%@", (arc4random_uniform(2) > 0 ? @"320/180" : @"180/320"), photoCategories[arc4random_uniform((uint32_t)photoCategories.count)]]]; 67 | [media addObject:medium]; 68 | } 69 | m.media = media; 70 | } else { 71 | m.text = linkTexts[arc4random_uniform((uint32_t)linkTexts.count)]; 72 | // m.text = texts[arc4random_uniform((uint32_t)texts.count)]; 73 | } 74 | return m; 75 | } 76 | 77 | @end 78 | 79 | 80 | @implementation MessageMedium 81 | 82 | - (NSURL *)mxr_messenger_imageURLForSize:(CGSize)renderedSizeInPoints { 83 | return self.photoURL; 84 | } 85 | 86 | - (NSURL *)mxr_messenger_videoURLForSize:(CGSize)renderedSizeInPoints { 87 | return self.videoURL; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/Person.h: -------------------------------------------------------------------------------- 1 | // 2 | // Person.h 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface Person : NSObject 12 | 13 | @property (nonatomic, strong) NSString* name; 14 | @property (nonatomic, strong) NSURL* avatarURL; 15 | 16 | + (NSArray*)someRandomPeople; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/Person.m: -------------------------------------------------------------------------------- 1 | // 2 | // Person.m 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import "Person.h" 10 | 11 | @implementation Person 12 | 13 | + (NSArray *)someRandomPeople { 14 | NSArray* peopleData = @[@[@"Keanu Reeves", @"http://vignette2.wikia.nocookie.net/mst3k/images/0/0c/RiffTrax-_Keanu_Reeves_in_The_Matrix.jpg/revision/latest?cb=20140609075119"], 15 | @[@"Barack Obama", @"https://vignette4.wikia.nocookie.net/thefutureofeuropes/images/a/ae/Obama-head.png/revision/latest?cb=20140629212721"], 16 | @[@"Donald Trump", @"https://upload.wikimedia.org/wikipedia/commons/9/9e/Donald_Trump_crop_2015.jpeg"]]; 17 | NSMutableArray* allPeople = [[NSMutableArray alloc] init]; 18 | for (int i = 0; i < peopleData.count; i++) { 19 | Person* p = [[Person alloc] init]; 20 | p.name = peopleData[i][0]; 21 | p.avatarURL = [NSURL URLWithString:peopleData[i][1]]; 22 | [allPeople addObject:p]; 23 | } 24 | return allPeople; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/PersonCellNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // PersonCellNode.h 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "Person.h" 12 | 13 | @interface PersonCellNode : ASCellNode 14 | 15 | @property (nonatomic, strong) ASNetworkImageNode* avatarNode; 16 | @property (nonatomic, strong) ASTextNode* nameNode; 17 | 18 | - (instancetype)initWithPerson:(Person*)person; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/PersonCellNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // PersonCellNode.m 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import "PersonCellNode.h" 10 | 11 | @implementation PersonCellNode 12 | 13 | - (instancetype)initWithPerson:(Person *)person { 14 | self = [super init]; 15 | if (self) { 16 | self.automaticallyManagesSubnodes = YES; 17 | self.backgroundColor = [UIColor whiteColor]; 18 | _avatarNode = [[ASNetworkImageNode alloc] init]; 19 | _avatarNode.style.preferredSize = CGSizeMake(30, 30); 20 | _avatarNode.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(0, nil); 21 | _avatarNode.URL = person.avatarURL; 22 | _nameNode = [[ASTextNode alloc] init]; 23 | _nameNode.layerBacked = YES; 24 | _nameNode.attributedText = [[NSAttributedString alloc] initWithString:person.name attributes:nil]; 25 | } 26 | return self; 27 | } 28 | 29 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 30 | ASStackLayoutSpec* contentStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; 31 | contentStack.spacing = 8.0f; 32 | contentStack.children = @[_avatarNode, _nameNode]; 33 | return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(8.0, 20.0f, 8.0f, 20.0f) child:contentStack]; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Examples/Example1/Example1/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Example1 4 | // 5 | // Created by Scott Kensell on 5/1/17. 6 | // Copyright © 2017 Scott Kensell. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/Example1/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'Example1' do 4 | pod 'MXRMessenger', :path => '../../' 5 | pod 'Texture' # forces PIN too 6 | end 7 | -------------------------------------------------------------------------------- /Examples/Example1/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - MXRMessenger (0.1.3): 3 | - MXRMessenger/Core (= 0.1.3) 4 | - MXRMessenger/MessageCell (= 0.1.3) 5 | - MXRMessenger/ViewController (= 0.1.3) 6 | - MXRMessenger/Core (0.1.3): 7 | - Texture/Core (~> 2.0) 8 | - MXRMessenger/MessageCell (0.1.3): 9 | - MXRMessenger/Core 10 | - MXRMessenger/ViewController (0.1.3): 11 | - MXRMessenger/Core 12 | - PINCache (3.0.1-beta.5): 13 | - PINCache/Arc-exception-safe (= 3.0.1-beta.5) 14 | - PINCache/Core (= 3.0.1-beta.5) 15 | - PINCache/Arc-exception-safe (3.0.1-beta.5): 16 | - PINCache/Core 17 | - PINCache/Core (3.0.1-beta.5): 18 | - PINOperation (= 1.0.3) 19 | - PINOperation (1.0.3) 20 | - PINRemoteImage/Core (3.0.0-beta.12): 21 | - PINOperation 22 | - PINRemoteImage/iOS (3.0.0-beta.12): 23 | - PINRemoteImage/Core 24 | - PINRemoteImage/PINCache (3.0.0-beta.12): 25 | - PINCache (= 3.0.1-beta.5) 26 | - PINRemoteImage/Core 27 | - Texture (2.5.1): 28 | - Texture/PINRemoteImage (= 2.5.1) 29 | - Texture/Core (2.5.1) 30 | - Texture/PINRemoteImage (2.5.1): 31 | - PINRemoteImage/iOS (= 3.0.0-beta.12) 32 | - PINRemoteImage/PINCache 33 | - Texture/Core 34 | 35 | DEPENDENCIES: 36 | - MXRMessenger (from `../../`) 37 | - Texture 38 | 39 | EXTERNAL SOURCES: 40 | MXRMessenger: 41 | :path: ../../ 42 | 43 | SPEC CHECKSUMS: 44 | MXRMessenger: 1224c9a7f7056f65b8c12b05771fe2b9fbdeb4dd 45 | PINCache: 98e7b1ef782824ad231ade51987c218b758c30d8 46 | PINOperation: ac23db44796d4a564ecf9b5ea7163510f579b71d 47 | PINRemoteImage: 0cefe720c2612960bd360710efbd6c73a1991d5f 48 | Texture: c3f06e344aa091667b7a56002e525cde674d021a 49 | 50 | PODFILE CHECKSUM: e52e2f58a5b4104966f0b955bfb80f444bc70c1f 51 | 52 | COCOAPODS: 1.3.1 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Scott Kensell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MXRMessenger.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint MXRMessenger.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'MXRMessenger' 11 | s.version = '0.2.1' 12 | s.summary = 'MXRMessenger is a lightweight UI chat component built on top of Texture.' 13 | 14 | s.description = <<-DESC 15 | MXRMessenger is a lightweight UI chat component built on top of Texture (formerly AsyncDisplayKit). 16 | The ViewController subspec features a pleasantly dismissable keyboard accessorry view and toolbar. 17 | The MessageCell subspec features a Facebook-style chat bubble-grouping system. 18 | DESC 19 | 20 | s.homepage = 'https://github.com/skensell/MXRMessenger' 21 | s.license = { :type => 'MIT', :file => 'LICENSE' } 22 | s.author = { 'Scott Kensell' => 'skensell@gmail.com' } 23 | s.source = { :git => 'https://github.com/skensell/MXRMessenger.git', :tag => s.version.to_s } 24 | 25 | s.ios.deployment_target = '8.0' 26 | 27 | s.subspec 'Core' do |core| 28 | core.public_header_files = [ 29 | 'MXRMessenger/Core/**/*.h' 30 | ] 31 | core.source_files = [ 32 | 'MXRMessenger/Core/**/*.{h,m}' 33 | ] 34 | core.dependency 'Texture/Core', '~> 2.0' 35 | end 36 | 37 | s.subspec 'ViewController' do |vc| 38 | vc.public_header_files = [ 39 | 'MXRMessenger/ViewController/**/*.h' 40 | ] 41 | vc.source_files = [ 42 | 'MXRMessenger/ViewController/**/*.{h,m}' 43 | ] 44 | vc.dependency 'MXRMessenger/Core' 45 | end 46 | 47 | s.subspec 'MessageCell' do |mc| 48 | mc.public_header_files = [ 49 | 'MXRMessenger/MessageCell/**/*.h' 50 | ] 51 | mc.source_files = [ 52 | 'MXRMessenger/MessageCell/**/*.{h,m}' 53 | ] 54 | mc.dependency 'MXRMessenger/Core' 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /MXRMessenger/Core/MXRGrowingEditableTextNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRGrowingEditableTextNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/5/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /** 12 | * An editable text node which observes changes to the underlying textView's 13 | * text and notifies whenever a line break occurs. 14 | */ 15 | @interface MXRGrowingEditableTextNode : ASEditableTextNode 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /MXRMessenger/Core/MXRGrowingEditableTextNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRGrowingEditableTextNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/5/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MXRGrowingEditableTextNode() 12 | 13 | @property (nonatomic, assign) CGFloat previouslyCalculatedHeight; 14 | @property (nonatomic, strong) id textViewObservingToken; 15 | 16 | @end 17 | 18 | @implementation MXRGrowingEditableTextNode 19 | 20 | - (void)didLoad { 21 | [super didLoad]; 22 | __weak typeof(self) weakSelf = self; 23 | self.textViewObservingToken = [[NSNotificationCenter defaultCenter] addObserverForName:UITextViewTextDidChangeNotification object:self.textView queue:nil usingBlock:^(NSNotification * _Nonnull note) { 24 | [weakSelf calculateNewFrameAndNotify]; 25 | }]; 26 | } 27 | 28 | - (void)dealloc { 29 | [[NSNotificationCenter defaultCenter] removeObserver:_textViewObservingToken]; 30 | } 31 | 32 | - (void)calculateNewFrameAndNotify { 33 | if (!self.isNodeLoaded) return; 34 | CGFloat newHeight = [self frameForTextRange:NSMakeRange(0, self.textView.text.length)].size.height; 35 | if (fabs(newHeight - _previouslyCalculatedHeight) > 1) { 36 | self.previouslyCalculatedHeight = newHeight; 37 | [self setNeedsLayout]; 38 | } 39 | } 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /MXRMessenger/Core/MXRMessenger.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessenger.h 3 | // Pods 4 | // 5 | // Created by Scott Kensell on 8/28/17. 6 | // 7 | // 8 | 9 | #ifndef MXRMessenger_h 10 | #define MXRMessenger_h 11 | 12 | #ifndef MXR_MESSAGE_CELL 13 | #define MXR_MESSAGE_CELL __has_include() 14 | #endif 15 | 16 | #ifndef MXR_MESSENGER_VC 17 | #define MXR_MESSENGER_VC __has_include() 18 | #endif 19 | 20 | // core 21 | #import 22 | #import 23 | #import 24 | #import 25 | #import 26 | 27 | 28 | // message cell 29 | #if MXR_MESSAGE_CELL 30 | #import 31 | #endif 32 | 33 | 34 | // view controller 35 | #if MXR_MESSENGER_VC 36 | #import 37 | #import 38 | #import 39 | #endif 40 | 41 | #endif /* MXRMessenger_h */ 42 | -------------------------------------------------------------------------------- /MXRMessenger/Core/MXRMessengerMedium.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessengerMedium.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 4/4/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #ifndef MXRMessengerMedium_h 10 | #define MXRMessengerMedium_h 11 | 12 | #import 13 | #import 14 | 15 | @protocol MXRMessengerMedium 16 | 17 | - (NSURL*)mxr_messenger_imageURLForSize:(CGSize)renderedSizeInPoints; 18 | - (NSURL*)mxr_messenger_videoURLForSize:(CGSize)renderedSizeInPoints; 19 | 20 | @end 21 | 22 | #endif /* MXRMessengerMedium_h */ 23 | -------------------------------------------------------------------------------- /MXRMessenger/Core/UIBezierPath+MXRMessenger.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIBezierPath+MXRMessenger.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIBezierPath (MXRMessenger) 12 | 13 | // Uses a cache to prevent creating tons of UIBezierPath objects. 14 | // Adapted from ASDK method of similar name 15 | + (UIBezierPath*)mxr_bezierPathForRoundedRectWithCorners:(UIRectCorner)roundedCorners radius:(CGFloat)cornerRadius size:(CGSize)size; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /MXRMessenger/Core/UIBezierPath+MXRMessenger.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIBezierPath+MXRMessenger.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @implementation UIBezierPath (MXRMessenger) 12 | 13 | + (UIBezierPath *)mxr_bezierPathForRoundedRectWithCorners:(UIRectCorner)roundedCorners radius:(CGFloat)cornerRadius size:(CGSize)size { 14 | static NSCache *__pathCache = nil; 15 | static dispatch_once_t onceToken; 16 | dispatch_once(&onceToken, ^{ 17 | __pathCache = [[NSCache alloc] init]; 18 | // Comment from ASDK file: 19 | // UIBezierPath objects are fairly small and these are equally sized. 20 should be plenty for many different parameters. 20 | __pathCache.countLimit = 20; 21 | }); 22 | typedef struct { 23 | UIRectCorner corners; 24 | CGFloat radius; 25 | CGSize size; 26 | } PathKey; 27 | PathKey key = { roundedCorners, cornerRadius, size }; 28 | NSValue *pathKeyObject = [[NSValue alloc] initWithBytes:&key objCType:@encode(PathKey)]; 29 | CGSize cornerRadii = CGSizeMake(cornerRadius, cornerRadius); 30 | UIBezierPath *path = [__pathCache objectForKey:pathKeyObject]; 31 | if (path == nil) { 32 | path = [UIBezierPath bezierPathWithRoundedRect:(CGRect){CGPointZero, size} byRoundingCorners:roundedCorners cornerRadii:cornerRadii]; 33 | [__pathCache setObject:path forKey:pathKeyObject]; 34 | } 35 | return path; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /MXRMessenger/Core/UIColor+MXRMessenger.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+MXRMessenger.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIColor (MXRMessenger) 12 | 13 | + (UIColor*)mxr_bubbleLightGrayColor; 14 | + (UIColor*)mxr_fbMessengerBlue; 15 | 16 | - (UIColor *)mxr_lighterColor; 17 | - (UIColor *)mxr_darkerColor; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /MXRMessenger/Core/UIColor+MXRMessenger.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+MXRMessenger.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @implementation UIColor (MXRMessenger) 12 | 13 | + (UIColor *)mxr_bubbleLightGrayColor { 14 | return [UIColor colorWithHue:0.6666f saturation:0.02f brightness:0.92f alpha:1.0f]; 15 | } 16 | 17 | + (UIColor *)mxr_fbMessengerBlue { 18 | return [UIColor colorWithRed:0.00 green:0.52 blue:1.00 alpha:1.0]; 19 | } 20 | 21 | - (UIColor *)mxr_lighterColor { 22 | CGFloat h, s, b, a; 23 | if ([self getHue:&h saturation:&s brightness:&b alpha:&a]) 24 | return [UIColor colorWithHue:h saturation:s brightness:MIN(b * 1.3, 1.0) alpha:a]; 25 | return nil; 26 | } 27 | 28 | - (UIColor *)mxr_darkerColor { 29 | CGFloat h, s, b, a; 30 | if ([self getHue:&h saturation:&s brightness:&b alpha:&a]) 31 | return [UIColor colorWithHue:h saturation:s brightness:(b * 0.75) alpha:a]; 32 | return nil; 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /MXRMessenger/Core/UIImage+MXRMessenger.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+MXRMessenger.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | @interface UIImage (MXRMessenger) 14 | 15 | + (UIImage *)mxr_fromColor:(UIColor*)color; 16 | 17 | + (UIImage *)mxr_bubbleImageWithMaximumCornerRadius:(CGFloat)maxCornerRadius 18 | minimumCornerRadius:(CGFloat)minCornerRadius 19 | color:(UIColor *)fillColor 20 | cornersToApplyMaxRadius:(UIRectCorner)roundedCorners; 21 | 22 | + (asimagenode_modification_block_t)mxr_imageModificationBlockToScaleToSize:(CGSize)size cornerRadius:(CGFloat)cornerRadius; 23 | + (asimagenode_modification_block_t)mxr_imageModificationBlockToScaleToSize:(CGSize)size 24 | maximumCornerRadius:(CGFloat)maxCornerRadius 25 | minimumCornerRadius:(CGFloat)minCornerRadius 26 | borderColor:(UIColor*)borderColor 27 | borderWidth:(CGFloat)borderWidth 28 | cornersToApplyMaxRadius:(UIRectCorner)roundedCorners; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /MXRMessenger/Core/UIImage+MXRMessenger.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+MXRMessenger.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | @implementation UIImage (MXRMessenger) 14 | 15 | + (UIImage *)mxr_fromColor:(UIColor *)color { 16 | CGRect rect = CGRectMake(0, 0, 1, 1); 17 | UIGraphicsBeginImageContext(rect.size); 18 | CGContextRef context = UIGraphicsGetCurrentContext(); 19 | CGContextSetFillColorWithColor(context, [color CGColor]); 20 | CGContextFillRect(context, rect); 21 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 22 | UIGraphicsEndImageContext(); 23 | return image; 24 | } 25 | 26 | + (UIImage *)mxr_bubbleImageWithMaximumCornerRadius:(CGFloat)maxCornerRadius minimumCornerRadius:(CGFloat)minCornerRadius color:(UIColor *)fillColor cornersToApplyMaxRadius:(UIRectCorner)roundedCorners { 27 | CGFloat smallestImageHeight = (maxCornerRadius * 2) + 1; 28 | CGSize smallestImageSize = CGSizeMake(smallestImageHeight, smallestImageHeight); 29 | UIBezierPath* clippingPath = [UIBezierPath mxr_bezierPathForRoundedRectWithCorners:UIRectCornerAllCorners radius:minCornerRadius size:smallestImageSize]; 30 | UIBezierPath* path = [UIBezierPath mxr_bezierPathForRoundedRectWithCorners:roundedCorners radius:maxCornerRadius size:smallestImageSize]; 31 | 32 | UIGraphicsBeginImageContextWithOptions(clippingPath.bounds.size, NO, 0.0f); 33 | 34 | [clippingPath addClip]; 35 | [fillColor setFill]; 36 | [path fillWithBlendMode:kCGBlendModeCopy alpha:1]; 37 | 38 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 39 | UIGraphicsEndImageContext(); 40 | 41 | UIEdgeInsets capInsets = UIEdgeInsetsMake(maxCornerRadius, maxCornerRadius, maxCornerRadius, maxCornerRadius); 42 | result = [result resizableImageWithCapInsets:capInsets resizingMode:UIImageResizingModeStretch]; 43 | 44 | return result; 45 | } 46 | 47 | + (asimagenode_modification_block_t)mxr_imageModificationBlockToScaleToSize:(CGSize)size cornerRadius:(CGFloat)cornerRadius { 48 | return ^UIImage*(UIImage *originalImage){ 49 | UIGraphicsBeginImageContextWithOptions(size, NO, 0.0f); 50 | UIBezierPath *roundOutline = [UIBezierPath mxr_bezierPathForRoundedRectWithCorners:UIRectCornerAllCorners radius:cornerRadius size:size]; 51 | [roundOutline addClip]; 52 | [originalImage drawInRect:(CGRect){CGPointZero, size} blendMode:kCGBlendModeCopy alpha:1.0f]; 53 | UIImage *modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); 54 | UIGraphicsEndImageContext(); 55 | return modifiedImage; 56 | }; 57 | } 58 | 59 | + (asimagenode_modification_block_t)mxr_imageModificationBlockToScaleToSize:(CGSize)size maximumCornerRadius:(CGFloat)maxCornerRadius minimumCornerRadius:(CGFloat)minCornerRadius borderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth cornersToApplyMaxRadius:(UIRectCorner)roundedCorners { 60 | return ^UIImage*(UIImage *originalImage){ 61 | UIGraphicsBeginImageContextWithOptions(size, NO, 0.0f); 62 | UIBezierPath *outerOutline = [UIBezierPath mxr_bezierPathForRoundedRectWithCorners:UIRectCornerAllCorners radius:minCornerRadius size:size]; 63 | [outerOutline addClip]; 64 | UIBezierPath *innerOutline = [UIBezierPath mxr_bezierPathForRoundedRectWithCorners:roundedCorners radius:maxCornerRadius size:size]; 65 | [innerOutline addClip]; 66 | 67 | [originalImage drawInRect:(CGRect){CGPointZero, size} blendMode:kCGBlendModeCopy alpha:1.0f]; 68 | 69 | if (borderWidth > 0.0) { 70 | [borderColor setStroke]; 71 | [outerOutline setLineWidth:borderWidth]; 72 | [outerOutline stroke]; 73 | [innerOutline setLineWidth:borderWidth]; 74 | [innerOutline stroke]; 75 | } 76 | 77 | UIImage *modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); 78 | UIGraphicsEndImageContext(); 79 | return modifiedImage; 80 | }; 81 | } 82 | 83 | @end 84 | 85 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageCell.h 3 | // Pods 4 | // 5 | // Created by Scott Kensell on 8/28/17. 6 | // 7 | // 8 | 9 | #ifndef MXRMessageCell_h 10 | #define MXRMessageCell_h 11 | 12 | #import "MXRMessageCellConstants.h" 13 | #import "MXRMessageCellFactory.h" 14 | #import "MXRMessageCellNode.h" 15 | #import "MXRMessageContentNode.h" 16 | #import "MXRMessageContentNodeDelegate.h" 17 | #import "MXRMessageDateFormatter.h" 18 | #import "MXRMessageImageNode.h" 19 | #import "MXRMessageMediaCollectionNode.h" 20 | #import "MXRMessageMediumCellNode.h" 21 | #import "MXRMessageNodeConfiguration.h" 22 | #import "MXRMessageTextNode.h" 23 | #import "MXRPlayButtonNode.h" 24 | 25 | #endif /* MXRMessageCell_h */ 26 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageCellConstants.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageCellConstants.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/26/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #ifndef MXRMessageCellConstants_h 10 | #define MXRMessageCellConstants_h 11 | 12 | /** 13 | * Determines which side to pin content to, and which side the avatar is on. 14 | */ 15 | typedef NS_ENUM(NSInteger, MXRMessageLayoutDirection) { 16 | /** 17 | * Avatar on Left. All content pinned to left. 18 | */ 19 | MXRMessageLayoutDirectionLeftToRight = 0, 20 | 21 | /** 22 | * Avatar on Right. All content pinned to right. 23 | */ 24 | MXRMessageLayoutDirectionRightToLeft = 1, 25 | }; 26 | 27 | typedef NS_ENUM(NSInteger, MXRMessageContentType) { 28 | MXRMessageContentTypeTextOnly = 0, 29 | MXRMessageContentTypeImageOnly = 1, 30 | MXRMessageContentTypeMediaCollectionOnly = 2, 31 | }; 32 | 33 | typedef NS_OPTIONS(NSInteger, MXRMessageMenuItemTypes) { 34 | MXRMessageMenuItemTypeNone = 0, 35 | MXRMessageMenuItemTypeCopy = (1 << 0), 36 | MXRMessageMenuItemTypeDelete = (1 << 1) 37 | }; 38 | 39 | #endif /* MXRMessageCellConstants_h */ 40 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageCellFactory.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageCellFactory.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/26/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageCellNode.h" 10 | 11 | @class MXRMessageAvatarConfiguration; 12 | @class MXRMessageCellConfiguration; 13 | @class MXRMessageCellFactory; 14 | @class MXRMessageDateFormatter; 15 | 16 | 17 | @protocol MXRMessageCellFactoryDataSource 18 | 19 | @required 20 | - (BOOL)cellFactory:(MXRMessageCellFactory*)cellFactory isMessageFromMeAtRow:(NSInteger)row; 21 | 22 | @optional 23 | - (NSURL*)cellFactory:(MXRMessageCellFactory*)cellFactory avatarURLAtRow:(NSInteger)row; 24 | - (NSTimeInterval)cellFactory:(MXRMessageCellFactory*)cellFactory timeIntervalSince1970AtRow:(NSInteger)row; 25 | 26 | @end 27 | 28 | 29 | @interface MXRMessageCellFactory : NSObject 30 | 31 | @property (nonatomic, strong, readonly) MXRMessageCellConfiguration* cellConfigForMe; 32 | @property (nonatomic, strong, readonly) MXRMessageCellConfiguration* cellConfigForOthers; 33 | 34 | @property (nonatomic, strong) MXRMessageDateFormatter* dateFormatter; 35 | 36 | @property (nonatomic, assign) BOOL isAutomaticallyManagingWhichCornersAreRounded; // Defaults to YES 37 | @property (nonatomic, assign) BOOL isAutomaticallyManagingDateHeaders; // Defaults to YES 38 | @property (nonatomic, assign) CGFloat spacingBetweenMessagesFromMeAndMessagesFromOthers; 39 | 40 | @property (nonatomic, weak) id dataSource; 41 | @property (nonatomic, weak) id contentNodeDelegate; 42 | @property (nonatomic, weak) id mediaCollectionDelegate; 43 | 44 | - (instancetype)initWithCellConfigForMe:(MXRMessageCellConfiguration*)cellConfigForMe cellConfigForOthers:(MXRMessageCellConfiguration*)cellConfigForOthers; 45 | 46 | - (MXRMessageTextCellNodeBlock)cellNodeBlockWithText:(NSString*)text tableNode:(ASTableNode*)tableNode row:(NSInteger)row; 47 | - (MXRMessageImageCellNodeBlock)cellNodeBlockWithImageURL:(NSURL*)imageURL showsPlayButton:(BOOL)showsPlayButton tableNode:(ASTableNode*)tableNode row:(NSInteger)row; 48 | - (MXRMessageMediaCollectionCellNodeBlock)cellNodeBlockWithMedia:(NSArray>*)media tableNode:(ASTableNode*)tableNode row:(NSInteger)row; 49 | 50 | - (ASDisplayNode*)headerNodeFromDate:(NSDate*)date; 51 | - (void)toggleDateHeaderNodeVisibilityForCellNode:(MXRMessageCellNode*)cellNode; 52 | - (void)updateTableNode:(ASTableNode*)tableNode animated:(BOOL)animated withInsertions:(NSArray*)insertions deletions:(NSArray*)deletions reloads:(NSArray*)reloads completion:(void(^)(BOOL))completion; 53 | 54 | @end 55 | 56 | 57 | @interface MXRMessageAvatarConfiguration : NSObject 58 | 59 | @property (nonatomic, assign) CGSize size; 60 | @property (nonatomic, assign) CGFloat cornerRadius; 61 | 62 | @end 63 | 64 | 65 | @interface MXRMessageCellConfiguration : NSObject 66 | 67 | @property (nonatomic, strong, readonly) MXRMessageCellLayoutConfiguration* layoutConfig; 68 | @property (nonatomic, strong, readonly) MXRMessageAvatarConfiguration* avatarConfig; 69 | @property (nonatomic, strong, readonly) MXRMessageTextConfiguration* textConfig; 70 | @property (nonatomic, strong, readonly) MXRMessageImageConfiguration* imageConfig; 71 | @property (nonatomic, strong, readonly) MXRMessageMediaCollectionConfiguration* mediaCollectionConfig; 72 | 73 | - (instancetype)initWithLayoutConfig:(MXRMessageCellLayoutConfiguration*)layoutConfig avatarConfig:(MXRMessageAvatarConfiguration*)avatarConfig textConfig:(MXRMessageTextConfiguration*)textConfig imageConfig:(MXRMessageImageConfiguration*)imageConfig mediaCollectionConfig:(MXRMessageMediaCollectionConfiguration*)mediaCollectionConfig; 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageCellFactory.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageCellFactory.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/26/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageCellFactory.h" 10 | 11 | #import "MXRMessageImageNode.h" 12 | #import "MXRMessageDateFormatter.h" 13 | #import "UIImage+MXRMessenger.h" 14 | #import "UIColor+MXRMessenger.h" 15 | 16 | typedef struct MXRMessageContext { 17 | // We use pointers so that NULL can represent no previous and no next. 18 | // the actual pointer should be handled with caution since it may point to garbage 19 | // stack addresses. We don't attempt to keep a properly synced linked list. 20 | struct MXRMessageContext* previous; 21 | struct MXRMessageContext* next; 22 | BOOL isFromMe; 23 | BOOL isShowingDate; 24 | NSTimeInterval timestamp; 25 | UIRectCorner cornersHavingRadius; 26 | } MXRMessageContext; 27 | 28 | static inline BOOL MXRMessageContextPreviousHasSameSender(MXRMessageContext c) { return c.previous != NULL && (c.previous->isFromMe == c.isFromMe); }; 29 | static inline BOOL MXRMessageContextNextHasSameSender(MXRMessageContext c) { return c.next != NULL && (c.next->isFromMe == c.isFromMe); }; 30 | static inline BOOL MXRMessageContextNextShowsDate(MXRMessageContext c) { return c.next != NULL && c.next->isShowingDate; }; 31 | 32 | @implementation MXRMessageCellFactory { 33 | struct { 34 | unsigned int isMessageFromMeAtRow:1; 35 | unsigned int avatarURLAtRow:1; 36 | unsigned int timestampAtRow:1; 37 | } _dataSourceFlags; 38 | } 39 | 40 | - (instancetype)initWithCellConfigForMe:(MXRMessageCellConfiguration *)cellConfigForMe cellConfigForOthers:(MXRMessageCellConfiguration *)cellConfigForOthers { 41 | self = [super init]; 42 | if (self) { 43 | _cellConfigForMe = cellConfigForMe ? : [[MXRMessageCellConfiguration alloc] init]; 44 | _cellConfigForOthers = cellConfigForOthers ? : [[MXRMessageCellConfiguration alloc] init]; 45 | _spacingBetweenMessagesFromMeAndMessagesFromOthers = 8.0f; 46 | 47 | _isAutomaticallyManagingWhichCornersAreRounded = YES; 48 | _isAutomaticallyManagingDateHeaders = YES; 49 | 50 | _dateFormatter = [[MXRMessageDateFormatter alloc] init]; 51 | } 52 | return self; 53 | } 54 | 55 | - (void)setDataSource:(id)dataSource { 56 | _dataSource = dataSource; 57 | if (_dataSource == nil) { 58 | memset(&_dataSourceFlags, 0, sizeof(_dataSourceFlags)); 59 | } else { 60 | _dataSourceFlags.isMessageFromMeAtRow = [_dataSource respondsToSelector:@selector(cellFactory:isMessageFromMeAtRow:)]; 61 | _dataSourceFlags.avatarURLAtRow = [_dataSource respondsToSelector:@selector(cellFactory:avatarURLAtRow:)]; 62 | _dataSourceFlags.timestampAtRow = [_dataSource respondsToSelector:@selector(cellFactory:timeIntervalSince1970AtRow:)]; 63 | NSAssert(!self.isAutomaticallyManagingWhichCornersAreRounded || _dataSourceFlags.isMessageFromMeAtRow, @"You must supply a dataSource which implements cellFactory:isMessageFromMeAtRow: if you want cellFactory to automatically manage corners."); 64 | NSAssert(!self.isAutomaticallyManagingDateHeaders || _dataSourceFlags.timestampAtRow, @"CellFactory has isAutomaticallyManagingDateHeaders=YES but you forgot to implement the necessary dataSource method to provide the date of a message."); 65 | } 66 | } 67 | 68 | - (MXRMessageTextCellNodeBlock)cellNodeBlockWithText:(NSString *)text tableNode:(ASTableNode *)tableNode row:(NSInteger)row { 69 | return (MXRMessageTextCellNodeBlock)[self cellNodeBlockWithType:MXRMessageContentTypeTextOnly text:text imageURL:nil showsPlayButton:NO media:nil tableNode:tableNode row:row]; 70 | } 71 | 72 | - (MXRMessageImageCellNodeBlock)cellNodeBlockWithImageURL:(NSURL *)imageURL showsPlayButton:(BOOL)showsPlayButton tableNode:(ASTableNode *)tableNode row:(NSInteger)row { 73 | return (MXRMessageImageCellNodeBlock)[self cellNodeBlockWithType:MXRMessageContentTypeImageOnly text:nil imageURL:imageURL showsPlayButton:showsPlayButton media:nil tableNode:tableNode row:row]; 74 | } 75 | 76 | - (MXRMessageMediaCollectionCellNodeBlock)cellNodeBlockWithMedia:(NSArray> *)media tableNode:(ASTableNode *)tableNode row:(NSInteger)row { 77 | return (MXRMessageMediaCollectionCellNodeBlock)[self cellNodeBlockWithType:MXRMessageContentTypeMediaCollectionOnly text:nil imageURL:nil showsPlayButton:YES media:media tableNode:tableNode row:row]; 78 | } 79 | 80 | - (ASCellNodeBlock)cellNodeBlockWithType:(MXRMessageContentType)type text:(NSString *)text imageURL:(NSURL*)imageURL showsPlayButton:(BOOL)showsPlayButton media:(NSArray> *)media tableNode:(ASTableNode *)tableNode row:(NSInteger)row { 81 | // we query the datasource before entering block, all other computations can go in the async block 82 | 83 | __block MXRMessageContext context; __block MXRMessageContext previousContext; __block MXRMessageContext nextContext; 84 | NSURL* avatarURL = nil; 85 | [self setContext:&context previous:&previousContext next:&nextContext avatarURL:&avatarURL tableNode:tableNode row:row]; 86 | 87 | __weak MXRMessageCellFactory* weakSelf = self; 88 | return ^MXRMessageCellNode*{ 89 | __strong MXRMessageCellFactory* self = weakSelf; 90 | // we reset pointers to prevent pointing to a garbage stack address 91 | if (context.previous != NULL) context.previous = &previousContext; 92 | if (context.next != NULL) context.next = &nextContext; 93 | 94 | MXRMessageCellConfiguration* config = context.isFromMe ? self.cellConfigForMe : self.cellConfigForOthers; 95 | MXRMessageCellLayoutConfiguration* layoutConfig = config.layoutConfig; 96 | MXRMessageAvatarConfiguration* avatarConfig = config.avatarConfig; 97 | 98 | if (!MXRMessageContextPreviousHasSameSender(context) && self.spacingBetweenMessagesFromMeAndMessagesFromOthers > 0.0f) { 99 | layoutConfig = [layoutConfig copy]; // we could cache this copy? maybe overoptimized then 100 | UIEdgeInsets newFinalInset = layoutConfig.finalInset; 101 | newFinalInset.top += self.spacingBetweenMessagesFromMeAndMessagesFromOthers; 102 | layoutConfig.finalInset = newFinalInset; 103 | } 104 | MXRMessageCellNode* cell = [[MXRMessageCellNode alloc] initWithLayoutConfiguration:layoutConfig]; 105 | 106 | if (avatarConfig && avatarConfig.size.width > 0 && avatarURL) { 107 | ASNetworkImageNode* avatarNode = [self networkImageNode]; 108 | avatarNode.contentMode = UIViewContentModeScaleAspectFill; 109 | avatarNode.style.preferredSize = avatarConfig.size; 110 | if (avatarConfig.cornerRadius > 0) { 111 | avatarNode.imageModificationBlock = [UIImage mxr_imageModificationBlockToScaleToSize:avatarConfig.size cornerRadius:avatarConfig.cornerRadius]; 112 | } 113 | avatarNode.URL = avatarURL; 114 | avatarNode.hidden = MXRMessageContextNextHasSameSender(context); 115 | cell.avatarNode = avatarNode; 116 | } 117 | 118 | UIRectCorner cornersHavingRadius = context.cornersHavingRadius; 119 | if (type == MXRMessageContentTypeTextOnly) { 120 | MXRMessageTextNode* textNode = [[MXRMessageTextNode alloc] initWithText:text configuration:config.textConfig cornersToApplyMaxRadius:cornersHavingRadius]; 121 | textNode.delegate = self.contentNodeDelegate; 122 | cell.messageContentNode = textNode; 123 | } else if (type == MXRMessageContentTypeImageOnly) { 124 | MXRMessageImageNode* imageNode = [[MXRMessageImageNode alloc] initWithImageURL:imageURL configuration:config.imageConfig cornersToApplyMaxRadius:cornersHavingRadius showsPlayButton:showsPlayButton]; 125 | imageNode.delegate = self.contentNodeDelegate; 126 | cell.messageContentNode = imageNode; 127 | } else if (type == MXRMessageContentTypeMediaCollectionOnly) { 128 | MXRMessageMediaCollectionNode* mediaCollectionNode = [[MXRMessageMediaCollectionNode alloc] initWithMedia:media configuration:config.mediaCollectionConfig cornersToApplyMaxRadius:cornersHavingRadius]; 129 | mediaCollectionNode.mediaCollectionDelegate = self.mediaCollectionDelegate; 130 | cell.messageContentNode = mediaCollectionNode; 131 | } 132 | 133 | if (context.isShowingDate) { 134 | cell.headerNode = [self headerNodeFromDate:[NSDate dateWithTimeIntervalSince1970:context.timestamp]]; 135 | } 136 | 137 | return cell; 138 | }; 139 | } 140 | 141 | - (ASNetworkImageNode*)networkImageNode { 142 | MXRMessageImageConfiguration* imageConfig = self.cellConfigForMe.imageConfig; 143 | if (imageConfig.imageCache && imageConfig.imageDownloader) { 144 | return [[ASNetworkImageNode alloc] initWithCache:imageConfig.imageCache downloader:imageConfig.imageDownloader]; 145 | } else { 146 | return [[ASNetworkImageNode alloc] init]; 147 | } 148 | } 149 | 150 | - (void)setContext:(MXRMessageContext*)context previous:(MXRMessageContext*)previousContext next:(MXRMessageContext*)nextContext avatarURL:(NSURL**)avatarURLHandle tableNode:(ASTableNode*)tableNode row:(NSInteger)row { 151 | // This method initializes the values of context and its pointers, but does not attempt to construct 152 | // a proper linked list by initializing the pointers in previousContext and nextContext. 153 | // Only the non-pointer properties in previous and next should be accessed. 154 | 155 | NSAssert(context, @"internal expectations failed: no context"); 156 | NSAssert(previousContext, @"internal expectations failed: no previousContext"); 157 | NSAssert(nextContext, @"internal expectations failed: no nextContext"); 158 | 159 | // bc of ASTableNode.inverted=YES the most recent message is at 0,0 160 | BOOL hasPreviousMessage = row < [self.dataSource tableNode:tableNode numberOfRowsInSection:0] - 1; 161 | BOOL hasNextMessage = row > 0; 162 | 163 | context->previous = hasPreviousMessage ? previousContext : NULL; 164 | context->next = hasNextMessage ? nextContext : NULL; 165 | 166 | if (self.isAutomaticallyManagingWhichCornersAreRounded) { 167 | previousContext->isFromMe = hasPreviousMessage ? [self.dataSource cellFactory:self isMessageFromMeAtRow:(row + 1)] : NO; 168 | context->isFromMe = [self.dataSource cellFactory:self isMessageFromMeAtRow:row]; 169 | nextContext->isFromMe = hasNextMessage ? [self.dataSource cellFactory:self isMessageFromMeAtRow:(row - 1)] : NO; 170 | } else { 171 | previousContext->isFromMe = context->isFromMe = nextContext->isFromMe = NO; 172 | } 173 | 174 | if (self.isAutomaticallyManagingDateHeaders) { 175 | previousContext->timestamp = hasPreviousMessage ? [self.dataSource cellFactory:self timeIntervalSince1970AtRow:(row + 1)] : 0; 176 | context->timestamp = [self.dataSource cellFactory:self timeIntervalSince1970AtRow:row]; 177 | nextContext->timestamp = hasNextMessage ? [self.dataSource cellFactory:self timeIntervalSince1970AtRow:(row - 1)] : 0; 178 | 179 | // we dont need to calculate if previous is showing date since we only show 180 | // dates in headers to determine corner rounding 181 | previousContext->isShowingDate = NO; 182 | context->isShowingDate = context->timestamp != 0 && (context->timestamp - previousContext->timestamp) > 900; 183 | nextContext->isShowingDate = nextContext->timestamp != 0 && ((nextContext->timestamp - context->timestamp) > 900); 184 | } else { 185 | previousContext->isShowingDate = context->isShowingDate = nextContext->isShowingDate = NO; 186 | previousContext->timestamp = context->timestamp = nextContext->timestamp = 0; 187 | } 188 | 189 | if ((avatarURLHandle != NULL) && _dataSourceFlags.avatarURLAtRow) { 190 | *avatarURLHandle = [self.dataSource cellFactory:self avatarURLAtRow:row]; 191 | } 192 | 193 | UIRectCorner cornersHavingRadius = UIRectCornerTopLeft | UIRectCornerTopRight | UIRectCornerBottomLeft | UIRectCornerBottomRight; 194 | if (self.isAutomaticallyManagingWhichCornersAreRounded) { 195 | MXRMessageLayoutDirection layoutDirection = context->isFromMe ? self.cellConfigForMe.layoutConfig.layoutDirection : self.cellConfigForOthers.layoutConfig.layoutDirection; 196 | if (MXRMessageContextNextHasSameSender(*context) && !MXRMessageContextNextShowsDate(*context)) { 197 | cornersHavingRadius ^= (layoutDirection == MXRMessageLayoutDirectionLeftToRight ? UIRectCornerBottomLeft : UIRectCornerBottomRight); 198 | } 199 | if (MXRMessageContextPreviousHasSameSender(*context) && !context->isShowingDate) { 200 | cornersHavingRadius ^= (layoutDirection == MXRMessageLayoutDirectionLeftToRight ? UIRectCornerTopLeft : UIRectCornerTopRight); 201 | } 202 | } 203 | context->cornersHavingRadius = cornersHavingRadius; 204 | } 205 | 206 | - (ASDisplayNode *)headerNodeFromDate:(NSDate *)date { 207 | ASTextNode* dateTextNode = [[ASTextNode alloc] init]; 208 | dateTextNode.layerBacked = YES; 209 | dateTextNode.attributedText = [self.dateFormatter attributedTextForDate:date]; 210 | dateTextNode.textContainerInset = UIEdgeInsetsMake(20, 0, 8, 0); 211 | return dateTextNode; 212 | } 213 | 214 | - (void)toggleDateHeaderNodeVisibilityForCellNode:(MXRMessageCellNode *)cellNode { 215 | NSIndexPath* indexPath = [cellNode indexPath]; 216 | if (cellNode.headerNode) { 217 | cellNode.headerNode = nil; 218 | } else { 219 | cellNode.headerNode = [self headerNodeFromDate:[NSDate dateWithTimeIntervalSince1970:[self.dataSource cellFactory:self timeIntervalSince1970AtRow:indexPath.row]]]; 220 | } 221 | [cellNode setNeedsLayout]; 222 | } 223 | 224 | - (void)updateTableNode:(ASTableNode *)tableNode animated:(BOOL)animated withInsertions:(NSArray *)insertions deletions:(NSArray *)deletions reloads:(NSArray *)reloads completion:(void (^)(BOOL))completion { 225 | 226 | NSInteger oldNumberOfRows = [tableNode numberOfRowsInSection:0]; 227 | MXRMessageCellNode* oldMostRecentNode = nil; 228 | MXRMessageCellNode* oldOldestNode = nil; 229 | if (oldNumberOfRows > 0) { 230 | oldMostRecentNode = [tableNode nodeForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; 231 | oldOldestNode = [tableNode nodeForRowAtIndexPath:[NSIndexPath indexPathForRow:(oldNumberOfRows - 1) inSection:0]]; 232 | } 233 | 234 | __weak typeof(self) weakSelf = self; 235 | UITableViewRowAnimation insertAnimation = insertions.count > 3 ? UITableViewRowAnimationFade : UITableViewRowAnimationTop; 236 | [tableNode performBatchAnimated:animated updates:^{ 237 | [tableNode deleteRowsAtIndexPaths:deletions withRowAnimation:UITableViewRowAnimationFade]; 238 | [tableNode insertRowsAtIndexPaths:insertions withRowAnimation:insertAnimation]; 239 | [tableNode reloadRowsAtIndexPaths:reloads withRowAnimation:UITableViewRowAnimationNone]; 240 | } completion:^(BOOL finished) { 241 | if (!finished) { 242 | if (completion) completion(finished); 243 | return; 244 | } 245 | 246 | if (weakSelf.isAutomaticallyManagingWhichCornersAreRounded) { 247 | [weakSelf updateRoundedCornersOfCellNode:oldMostRecentNode]; 248 | } 249 | 250 | NSIndexPath* oldOldestIndexPath = [oldOldestNode indexPath]; 251 | BOOL oldOldestNeedsReload = weakSelf.isAutomaticallyManagingDateHeaders && oldOldestIndexPath && oldOldestIndexPath.row != ([tableNode numberOfRowsInSection:0] - 1); 252 | if (oldOldestNeedsReload) { 253 | // We have to reload the oldest because it may erroneously show date headers after a tail load. 254 | // And it's not just dates, the top final inset can be wrong too. 255 | [tableNode performBatchAnimated:animated updates:^{ 256 | [tableNode reloadRowsAtIndexPaths:@[oldOldestIndexPath] withRowAnimation:UITableViewRowAnimationNone]; 257 | } completion:completion]; 258 | } else { 259 | if (completion) completion(finished); 260 | } 261 | }]; 262 | } 263 | 264 | - (void)updateRoundedCornersOfCellNode:(MXRMessageCellNode *)cellNode { 265 | NSIndexPath* indexPath = cellNode.indexPath; 266 | ASTableNode* tableNode = (ASTableNode*)cellNode.owningNode; 267 | if (!tableNode || !indexPath || !self.isAutomaticallyManagingWhichCornersAreRounded) return; 268 | 269 | __block MXRMessageContext context; __block MXRMessageContext previousContext; __block MXRMessageContext nextContext; 270 | [self setContext:&context previous:&previousContext next:&nextContext avatarURL:NULL tableNode:tableNode row:indexPath.row]; 271 | [cellNode.messageContentNode redrawBubbleWithCorners:context.cornersHavingRadius]; 272 | if (MXRMessageContextNextHasSameSender(context)) { 273 | cellNode.avatarNode.hidden = YES; 274 | } 275 | } 276 | 277 | @end 278 | 279 | 280 | @implementation MXRMessageAvatarConfiguration 281 | 282 | - (instancetype)init { 283 | self = [super init]; 284 | if (self) { 285 | _size = CGSizeMake(36, 36); 286 | _cornerRadius = 18.0f; 287 | } 288 | return self; 289 | } 290 | 291 | @end 292 | 293 | 294 | @implementation MXRMessageCellConfiguration 295 | 296 | - (instancetype)init { 297 | return [self initWithLayoutConfig:nil avatarConfig:nil textConfig:nil imageConfig:nil mediaCollectionConfig:nil]; 298 | } 299 | 300 | - (instancetype)initWithLayoutConfig:(MXRMessageCellLayoutConfiguration *)layoutConfig avatarConfig:(MXRMessageAvatarConfiguration *)avatarConfig textConfig:(MXRMessageTextConfiguration *)textConfig imageConfig:(MXRMessageImageConfiguration *)imageConfig mediaCollectionConfig:(MXRMessageMediaCollectionConfiguration *)mediaCollectionConfig { 301 | self = [super init]; 302 | if (self) { 303 | _layoutConfig = layoutConfig ? : [[MXRMessageCellLayoutConfiguration alloc] init]; 304 | _avatarConfig = avatarConfig; 305 | _textConfig = textConfig ? : [[MXRMessageTextConfiguration alloc] init]; 306 | _imageConfig = imageConfig ? : [[MXRMessageImageConfiguration alloc] init]; 307 | _mediaCollectionConfig = mediaCollectionConfig ? : [[MXRMessageMediaCollectionConfiguration alloc] init]; 308 | } 309 | return self; 310 | } 311 | 312 | @end 313 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageCellNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageCellNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/24/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "MXRMessageCellConstants.h" 12 | #import "MXRMessageTextNode.h" 13 | #import "MXRMessageImageNode.h" 14 | #import "MXRMessageMediaCollectionNode.h" 15 | 16 | @class MXRMessageCellLayoutConfiguration; 17 | 18 | /** 19 | * The suggested class to use for cells in the ASTableNode owned by `MXRMessengerViewController`. 20 | * 21 | * The easiest way to create these is to call the appropriate method of `MXRMessageCellFactory` in the ASTableDataSource method `-tableNode:nodeBlockForRowAtIndexPath:`. 22 | * 23 | * This class only provides the relative positioning of the header, message, avatar, and footer. 24 | * To use it, simply instantiate it and set your custom nodes as the properties 25 | * headerNode, messageContentNode, etc. 26 | */ 27 | @interface MXRMessageCellNode <__covariant ContentNodeType : MXRMessageContentNode*> : ASCellNode 28 | 29 | /** 30 | * The parameters necessary to layout a MXRMessageCellNode. 31 | */ 32 | @property (nonatomic, strong, readonly) MXRMessageCellLayoutConfiguration* layoutConfig; 33 | 34 | /** 35 | * Defaults to nil. If non-nil, this content appears above everything else. 36 | */ 37 | @property (nonatomic, strong) ASDisplayNode* headerNode; 38 | 39 | /** 40 | * Defaults to nil. You should set this to a node which displays the main message content, e.g. a textNode. 41 | */ 42 | @property (nonatomic, strong) ContentNodeType messageContentNode; 43 | 44 | /** 45 | * Defaults to nil. Set this if you want to display an avatar for a user. It will appear next to `messageContentNode`. 46 | */ 47 | @property (nonatomic, strong) ASNetworkImageNode* avatarNode; 48 | 49 | /** 50 | * Defaults to nil. If non-nil, this content appears below everything else. 51 | */ 52 | @property (nonatomic, strong) ASDisplayNode* footerNode; 53 | 54 | - (instancetype)initWithLayoutConfiguration:(MXRMessageCellLayoutConfiguration*)layoutConfig NS_DESIGNATED_INITIALIZER; 55 | 56 | @end 57 | 58 | typedef MXRMessageCellNode* (^MXRMessageCellNodeBlock)(void); 59 | 60 | typedef MXRMessageCellNode MXRMessageTextCellNode; 61 | typedef MXRMessageTextCellNode* (^MXRMessageTextCellNodeBlock)(void); 62 | 63 | typedef MXRMessageCellNode MXRMessageImageCellNode; 64 | typedef MXRMessageImageCellNode* (^MXRMessageImageCellNodeBlock)(void); 65 | 66 | typedef MXRMessageCellNode MXRMessageMediaCollectionCellNode; 67 | typedef MXRMessageMediaCollectionCellNode* (^MXRMessageMediaCollectionCellNodeBlock)(void); 68 | 69 | @interface MXRMessageCellLayoutConfiguration : NSObject 70 | 71 | /** 72 | * Whether to pin the content and avatar to the right or left. 73 | */ 74 | @property (nonatomic, assign) MXRMessageLayoutDirection layoutDirection; 75 | 76 | /** 77 | * The fraction of available width (minus avatar width and insets) which the `messageContentNode` will be restricted to. 78 | * However, this does not apply to MXRMessageImage node, for which you must specify a maximum size for images. 79 | */ 80 | @property (nonatomic, assign) CGFloat fractionOfWidthToLayoutContent; 81 | 82 | /** 83 | * The spacing between the avatar and the `messageContentNode`. Applicable only if `avatarNode` is set. 84 | */ 85 | @property (nonatomic, assign) CGFloat avatarToContentSpacing; 86 | 87 | /** 88 | * Insets to apply to the `avatarNode` and `messageContentNode` together. Does not affect `headerNode` and `footerNode`. 89 | */ 90 | @property (nonatomic, assign) UIEdgeInsets avatarAndContentInset; 91 | 92 | /** 93 | * The final insets to apply to all content appearing in `MXRMessageCellNode`. 94 | * Unlike `avatarAndContentInset` this affects `headerNode` and `footerNode` too. 95 | */ 96 | @property (nonatomic, assign) UIEdgeInsets finalInset; 97 | 98 | + (instancetype)leftToRight; 99 | + (instancetype)rightToLeft; 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageCellNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageCellNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/24/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageCellNode.h" 10 | 11 | @implementation MXRMessageCellNode 12 | 13 | - (instancetype)init { 14 | return [self initWithLayoutConfiguration:nil]; 15 | } 16 | 17 | - (instancetype)initWithLayoutConfiguration:(MXRMessageCellLayoutConfiguration *)layoutConfig { 18 | self = [super init]; 19 | if (self) { 20 | self.automaticallyManagesSubnodes = YES; 21 | self.selectionStyle = UITableViewCellSelectionStyleNone; 22 | _layoutConfig = layoutConfig; 23 | NSAssert(layoutConfig != nil, @"You must provide a layoutConfig at init to MXRMessageCellNode"); 24 | } 25 | return self; 26 | } 27 | 28 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 29 | NSAssert(!_avatarNode || !CGSizeEqualToSize(CGSizeZero, _avatarNode.style.preferredSize), @"avatarNode (if provided) must be sized with a non-zero preferredSize to be able to enforce maximum content width properly."); 30 | ASStackLayoutSpec* contentStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; 31 | contentStack.alignItems = ASStackLayoutAlignItemsEnd; 32 | contentStack.spacing = _layoutConfig.avatarToContentSpacing; 33 | NSMutableArray* contentChildren = [[NSMutableArray alloc] init]; 34 | if (_avatarNode) [contentChildren addObject:_avatarNode]; 35 | if (_messageContentNode) [contentChildren addObject:_messageContentNode]; 36 | 37 | if (_layoutConfig.layoutDirection == MXRMessageLayoutDirectionRightToLeft) { 38 | NSMutableArray* reversedContentChildren = [[NSMutableArray alloc] initWithCapacity:contentChildren.count]; 39 | [contentChildren enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 40 | [reversedContentChildren addObject:obj]; 41 | }]; 42 | contentChildren = reversedContentChildren; 43 | } 44 | 45 | if (CGSizeEqualToSize(_messageContentNode.style.preferredSize, CGSizeZero)) { 46 | // We enforce the maxWidth of the contentNode because it seems to be a bug in ASDK that 47 | // when a textNode is in a horizontal stack, 48 | // on the first layout pass, contentSize.width is infinite 49 | // on the 2nd layout pass, it's enforced by the flexShrink, flexGrow rules 50 | // there is a missing 3rd layout pass where the textNode should clamp to its text's rendered bounds 51 | CGFloat usedWidth = _layoutConfig.finalInset.left + _layoutConfig.finalInset.right + _layoutConfig.avatarAndContentInset.left + _layoutConfig.avatarAndContentInset.right + (_avatarNode ? (_layoutConfig.avatarToContentSpacing + _avatarNode.style.preferredSize.width) : 0); 52 | CGFloat maxContentWidth = (constrainedSize.max.width - usedWidth) * _layoutConfig.fractionOfWidthToLayoutContent; 53 | if (maxContentWidth > 0) { 54 | _messageContentNode.style.maxWidth = ASDimensionMakeWithPoints(maxContentWidth); 55 | } 56 | } 57 | 58 | contentStack.children = contentChildren; 59 | 60 | ASInsetLayoutSpec* contentInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:_layoutConfig.avatarAndContentInset child:contentStack]; 61 | 62 | ASStackLayoutSpec* headerContentAndFooterStack = [ASStackLayoutSpec verticalStackLayoutSpec]; 63 | headerContentAndFooterStack.alignItems = ASStackLayoutAlignItemsStretch; 64 | contentInset.style.alignSelf = _layoutConfig.layoutDirection == MXRMessageLayoutDirectionLeftToRight ? ASStackLayoutAlignSelfStart : ASStackLayoutAlignSelfEnd; 65 | NSMutableArray *a = [[NSMutableArray alloc] init]; 66 | if (_headerNode) [a addObject:_headerNode]; 67 | [a addObject:contentInset]; 68 | if (_footerNode) [a addObject:_footerNode]; 69 | headerContentAndFooterStack.children = a; 70 | 71 | return [ASInsetLayoutSpec insetLayoutSpecWithInsets:_layoutConfig.finalInset child:headerContentAndFooterStack]; 72 | } 73 | 74 | @end 75 | 76 | @implementation MXRMessageCellLayoutConfiguration 77 | 78 | - (instancetype)init { 79 | self = [super init]; 80 | if (self) { 81 | _layoutDirection = MXRMessageLayoutDirectionLeftToRight; 82 | _fractionOfWidthToLayoutContent = 0.80f; 83 | _avatarToContentSpacing = 6.0f; 84 | _avatarAndContentInset = UIEdgeInsetsMake(1.0, 8.0f, 1.0, 8.0f); 85 | _finalInset = UIEdgeInsetsZero; 86 | } 87 | return self; 88 | } 89 | 90 | - (instancetype)copy { 91 | MXRMessageCellLayoutConfiguration* config = [[[self class] alloc] init]; 92 | config.layoutDirection = self.layoutDirection; 93 | config.fractionOfWidthToLayoutContent = self.fractionOfWidthToLayoutContent; 94 | config.avatarToContentSpacing = self.avatarToContentSpacing; 95 | config.avatarAndContentInset = self.avatarAndContentInset; 96 | config.finalInset = self.finalInset; 97 | return config; 98 | } 99 | 100 | - (instancetype)copyWithZone:(NSZone *)zone { 101 | return [self copy]; 102 | } 103 | 104 | + (instancetype)leftToRight { 105 | MXRMessageCellLayoutConfiguration* config = [[MXRMessageCellLayoutConfiguration alloc] init]; 106 | config.layoutDirection = MXRMessageLayoutDirectionLeftToRight; 107 | return config; 108 | } 109 | 110 | + (instancetype)rightToLeft { 111 | MXRMessageCellLayoutConfiguration* config = [[MXRMessageCellLayoutConfiguration alloc] init]; 112 | config.layoutDirection = MXRMessageLayoutDirectionRightToLeft; 113 | return config; 114 | } 115 | 116 | @end 117 | 118 | 119 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageContentNode+Subclasses.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageContentNode+Subclasses.h 3 | // Pods 4 | // 5 | // Created by Scott Kensell on 12/7/17. 6 | // 7 | 8 | #ifndef MXRMessageContentNode_Subclasses_h 9 | #define MXRMessageContentNode_Subclasses_h 10 | 11 | @interface MXRMessageContentNode () 12 | 13 | @property (nonatomic, assign) NSTimeInterval touchStartTimestamp; 14 | 15 | @end 16 | 17 | #endif /* MXRMessageContentNode_Subclasses_h */ 18 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageContentNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageContentNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/31/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "MXRMessageContentNodeDelegate.h" 12 | #import "MXRMessageNodeConfiguration.h" 13 | 14 | @interface MXRMessageContentNode : ASDisplayNode 15 | 16 | @property (nonatomic, assign) MXRMessageMenuItemTypes menuItemTypes; 17 | @property (nonatomic, assign) BOOL showsUIMenuControllerOnLongTap; 18 | @property (nonatomic, assign) BOOL highlighted; 19 | @property (nonatomic, weak) id delegate; 20 | 21 | - (instancetype)initWithConfiguration:(MXRMessageNodeConfiguration*)configuration NS_DESIGNATED_INITIALIZER; 22 | 23 | // these call delegate by default, subclasses should call super 24 | - (void)copy:(id)sender; 25 | - (void)delete:(id)sender; 26 | 27 | - (void)redrawBubbleWithCorners:(UIRectCorner)cornersHavingRadius; // abstract, override 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageContentNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageContentNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/31/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageContentNode.h" 10 | 11 | #import "MXRMessageContentNode+Subclasses.h" 12 | 13 | @implementation MXRMessageContentNode 14 | 15 | - (instancetype)initWithConfiguration:(MXRMessageNodeConfiguration *)configuration { 16 | self = [super init]; 17 | if (self) { 18 | _menuItemTypes = configuration.menuItemTypes; 19 | _showsUIMenuControllerOnLongTap = configuration.showsUIMenuControllerOnLongTap; 20 | self.userInteractionEnabled = _showsUIMenuControllerOnLongTap; 21 | } 22 | return self; 23 | } 24 | 25 | - (instancetype)init { 26 | ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); 27 | return [self initWithConfiguration:nil]; 28 | } 29 | 30 | - (void)setDelegate:(id)delegate { 31 | _delegate = delegate; 32 | self.userInteractionEnabled = self.userInteractionEnabled || ([_delegate respondsToSelector:@selector(messageContentNode:didSingleTap:)] || [_delegate respondsToSelector:@selector(messageContentNode:didLongTap:)]); 33 | } 34 | 35 | - (void)redrawBubbleWithCorners:(UIRectCorner)cornersHavingRadius { NSAssert(NO, @"Abstract method not implemented: %@", NSStringFromSelector(_cmd)); } 36 | 37 | #pragma mark - UIResponderStandardEditActions 38 | 39 | - (BOOL)respondsToSelector:(SEL)aSelector { 40 | // see note in `canPerformAction:withSender:` 41 | if (aSelector == @selector(copy:) || aSelector == @selector(delete:)) { 42 | return [self canPerformAction:aSelector withSender:self]; 43 | } 44 | return [super respondsToSelector:aSelector]; 45 | } 46 | 47 | - (BOOL)canBecomeFirstResponder { 48 | return _menuItemTypes != MXRMessageMenuItemTypeNone; 49 | } 50 | 51 | - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { 52 | // Note: This is not being called because _ASDisplayView does not forward the call from UIView. 53 | // You can see in _ASDisplayView it checks to see if `action` is implemented on the node by calling 54 | // `respondsToSelector:`. Our workaround is to call this method from within `respondsToSelector:` 55 | // Calling super would create a loop I believe, so dont. 56 | return ((action == @selector(copy:) && (_menuItemTypes & MXRMessageMenuItemTypeCopy)) || 57 | (action == @selector(delete:) && (_menuItemTypes & MXRMessageMenuItemTypeDelete))); 58 | } 59 | 60 | - (void)copy:(id)sender { 61 | [_delegate messageContentNode:self didTapMenuItemWithType:MXRMessageMenuItemTypeCopy]; 62 | } 63 | 64 | - (void)delete:(id)sender { 65 | [_delegate messageContentNode:self didTapMenuItemWithType:MXRMessageMenuItemTypeDelete]; 66 | } 67 | 68 | 69 | #pragma mark - ASDisplayNode Overrides 70 | #pragma clang diagnostic push 71 | #pragma clang diagnostic ignored "-Wobjc-missing-super-calls" 72 | 73 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 74 | _touchStartTimestamp = event.timestamp; 75 | self.highlighted = YES; 76 | } 77 | 78 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { 79 | self.highlighted = NO; 80 | } 81 | 82 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 83 | } 84 | 85 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 86 | self.highlighted = NO; 87 | UITouch* touch = [touches anyObject]; 88 | BOOL isInside = [self pointInside:[touch locationInView:self.view] withEvent:event]; 89 | if (!isInside) return; 90 | CGFloat duration = event.timestamp - _touchStartTimestamp; 91 | if (duration > 0.35f) { 92 | // long tap 93 | if (_showsUIMenuControllerOnLongTap && [self becomeFirstResponder]) { 94 | [[UIMenuController sharedMenuController] setTargetRect:self.bounds inView:self.view]; 95 | [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES]; 96 | } 97 | if ([_delegate respondsToSelector:@selector(messageContentNode:didLongTap:)]) { 98 | [_delegate messageContentNode:self didLongTap:nil]; 99 | } 100 | } else { 101 | // tap 102 | if ([_delegate respondsToSelector:@selector(messageContentNode:didSingleTap:)]) { 103 | [_delegate messageContentNode:self didSingleTap:nil]; 104 | } 105 | } 106 | } 107 | 108 | #pragma clang diagnostic pop 109 | 110 | @end 111 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageContentNodeDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageContentNodeDelegate.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/31/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #ifndef MXRMessageContentNodeDelegate_h 10 | #define MXRMessageContentNodeDelegate_h 11 | 12 | #import "MXRMessageCellConstants.h" 13 | 14 | @class MXRMessageContentNode; 15 | 16 | @protocol MXRMessageContentNodeDelegate 17 | 18 | @optional 19 | - (void)messageContentNode:(MXRMessageContentNode*)node didSingleTap:(id)sender; 20 | - (void)messageContentNode:(MXRMessageContentNode*)node didLongTap:(id)sender; 21 | - (void)messageContentNode:(MXRMessageContentNode*)node didTapMenuItemWithType:(MXRMessageMenuItemTypes)menuItemType; 22 | - (void)messageContentNode:(MXRMessageContentNode*)node didTapURL:(NSURL*)url; 23 | - (void)messageContentNode:(MXRMessageContentNode*)node didLongTapURL:(NSURL*)url; 24 | 25 | @end 26 | 27 | #endif /* MXRMessageContentNodeDelegate_h */ 28 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageDateFormatter.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageDateFormatter.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/19/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol MXRMessageDateFormatter 12 | 13 | - (NSAttributedString *)attributedTextForDate:(NSDate *)date; 14 | 15 | @end 16 | 17 | 18 | @interface MXRMessageDateFormatter : NSObject 19 | 20 | @property (nonatomic, strong) NSDictionary *dateTextAttributes; // for the date portion 21 | @property (nonatomic, strong) NSDictionary *timeTextAttributes; // for the time portion 22 | @property (nonatomic, strong) NSDateFormatter* timestampFormatter; // for the time formatter portion 23 | 24 | @end 25 | 26 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageDateFormatter.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageDateFormatter.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/19/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageDateFormatter.h" 10 | 11 | #import "UIColor+MXRMessenger.h" 12 | 13 | @implementation MXRMessageDateFormatter { 14 | NSDateFormatter* _dateFormatter; 15 | NSDateFormatter* _todayOrYesterdayFormatter; 16 | NSDateFormatter* _thisWeekFormatter; 17 | } 18 | 19 | - (instancetype)init { 20 | self = [super init]; 21 | if (self) { 22 | _dateFormatter = [[NSDateFormatter alloc] init]; 23 | [_dateFormatter setLocale:[NSLocale currentLocale]]; 24 | [_dateFormatter setLocalizedDateFormatFromTemplate:@"MMMd"]; // see https://waracle.net/iphone-nsdateformatter-date-formatting-table/ 25 | 26 | _todayOrYesterdayFormatter = [[NSDateFormatter alloc] init]; 27 | [_todayOrYesterdayFormatter setLocale:[NSLocale currentLocale]]; 28 | [_todayOrYesterdayFormatter setDoesRelativeDateFormatting:YES]; 29 | [_todayOrYesterdayFormatter setDateStyle:NSDateFormatterMediumStyle]; 30 | [_todayOrYesterdayFormatter setTimeStyle:NSDateFormatterNoStyle]; 31 | 32 | _thisWeekFormatter = [[NSDateFormatter alloc] init]; 33 | [_thisWeekFormatter setLocale:[NSLocale currentLocale]]; 34 | [_thisWeekFormatter setLocalizedDateFormatFromTemplate:@"EEE"]; 35 | 36 | _timestampFormatter = [[NSDateFormatter alloc] init]; 37 | [_timestampFormatter setLocale:[NSLocale currentLocale]]; 38 | [_timestampFormatter setLocalizedDateFormatFromTemplate:@"Hmm"]; 39 | 40 | UIColor *color = [[UIColor mxr_bubbleLightGrayColor] mxr_darkerColor]; 41 | 42 | NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; 43 | paragraphStyle.alignment = NSTextAlignmentCenter; 44 | 45 | _dateTextAttributes = @{ NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0f], 46 | NSForegroundColorAttributeName : color, 47 | NSParagraphStyleAttributeName : paragraphStyle }; 48 | 49 | _timeTextAttributes = @{ NSFontAttributeName : [UIFont systemFontOfSize:12.0f], 50 | NSForegroundColorAttributeName : color, 51 | NSParagraphStyleAttributeName : paragraphStyle }; 52 | } 53 | return self; 54 | } 55 | 56 | #pragma mark - MXRMessageDateFormatter 57 | 58 | - (NSAttributedString *)attributedTextForDate:(NSDate *)date { 59 | if (!date) return nil; 60 | NSDateFormatter* dateFormatter = _dateFormatter; 61 | NSTimeInterval secondsAgo = [NSDate timeIntervalSinceReferenceDate] - date.timeIntervalSinceReferenceDate; 62 | if (secondsAgo < 86400) { 63 | dateFormatter = _todayOrYesterdayFormatter; 64 | } else if (secondsAgo < 518400) { 65 | dateFormatter = _thisWeekFormatter; 66 | } 67 | NSMutableAttributedString* result = [[NSMutableAttributedString alloc] initWithString:[dateFormatter stringFromDate:date] attributes:_dateTextAttributes]; 68 | [result appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]]; 69 | [result appendAttributedString:[[NSAttributedString alloc] initWithString:[_timestampFormatter stringFromDate:date] attributes:_timeTextAttributes]]; 70 | return result; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageImageNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageImageNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageContentNode.h" 10 | 11 | #import "MXRPlayButtonNode.h" 12 | 13 | @class MXRMessageImageConfiguration; 14 | 15 | @interface MXRMessageImageNode : MXRMessageContentNode 16 | 17 | @property (nonatomic, strong, readonly) ASNetworkImageNode* imageNode; 18 | @property (nonatomic, strong, readonly) MXRPlayButtonNode* playButtonNode; 19 | 20 | - (instancetype)initWithImageURL:(NSURL*)imageURL configuration:(MXRMessageImageConfiguration*)configuration cornersToApplyMaxRadius:(UIRectCorner)cornersHavingRadius showsPlayButton:(BOOL)showsPlayButton NS_DESIGNATED_INITIALIZER; 21 | 22 | @end 23 | 24 | 25 | @interface MXRMessageImageConfiguration : MXRMessageNodeConfiguration 26 | 27 | /** 28 | * If placeholder is nil, this size will still be used to display a plain white image 29 | * until the image is downloaded. 30 | */ 31 | @property (nonatomic, assign) CGSize placeholderImageSize; 32 | @property (nonatomic, strong) UIImage* placeholderImage; 33 | 34 | /** 35 | * MXRMessageImageNode at present does not support the fractionOfWidthToLayoutContent 36 | * of MXRMessageCellLayoutConfiguration. Here you can specify the box within which 37 | * the rendered image will be AspectFit. 38 | */ 39 | @property (nonatomic, assign) CGSize maximumImageSize; 40 | 41 | + (CGSize)suggestedMaxImageSizeForScreenSize:(CGSize)screenSize; 42 | 43 | @end 44 | 45 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageImageNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageImageNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageImageNode.h" 10 | 11 | #import "UIImage+MXRMessenger.h" 12 | #import "UIColor+MXRMessenger.h" 13 | 14 | @interface MXRMessageImageNode() 15 | 16 | @end 17 | 18 | @implementation MXRMessageImageNode { 19 | CGSize _maxSize; 20 | CGFloat _maxCornerRadius; 21 | CGFloat _minCornerRadius; 22 | UIRectCorner _cornersHavingMaxRadius; 23 | UIColor* _borderColor; 24 | CGFloat _borderWidth; 25 | } 26 | 27 | - (instancetype)initWithImageURL:(NSURL *)imageURL configuration:(MXRMessageImageConfiguration *)configuration cornersToApplyMaxRadius:(UIRectCorner)cornersHavingRadius showsPlayButton:(BOOL)showsPlayButton { 28 | self = [super initWithConfiguration:configuration]; 29 | if (self) { 30 | self.automaticallyManagesSubnodes = YES; 31 | _maxSize = configuration.maximumImageSize; 32 | _maxCornerRadius = configuration.maxCornerRadius; 33 | _minCornerRadius = configuration.minCornerRadius; 34 | _cornersHavingMaxRadius = cornersHavingRadius; 35 | _borderColor = configuration.borderColor; 36 | _borderWidth = configuration.borderWidth; 37 | 38 | if (configuration.imageCache && configuration.imageDownloader) { 39 | _imageNode = [[ASNetworkImageNode alloc] initWithCache:configuration.imageCache downloader:configuration.imageDownloader]; 40 | } else { 41 | _imageNode = [[ASNetworkImageNode alloc] init]; 42 | } 43 | [self setImageModificationBlockForSize:configuration.placeholderImageSize]; 44 | 45 | if (configuration.placeholderImage) { 46 | _imageNode.defaultImage = configuration.placeholderImage; 47 | } else { 48 | // We need a non-nil image so that we can see the border created in the imageModificationBlock 49 | _imageNode.defaultImage = [UIImage mxr_fromColor:[UIColor whiteColor]]; 50 | } 51 | _imageNode.style.preferredSize = configuration.placeholderImageSize; 52 | self.style.preferredSize = _imageNode.style.preferredSize; 53 | _imageNode.contentMode = UIViewContentModeScaleAspectFill; 54 | _imageNode.delegate = self; 55 | _imageNode.URL = imageURL; 56 | 57 | if (showsPlayButton) { 58 | _playButtonNode = [[MXRPlayButtonNode alloc] init]; 59 | } 60 | 61 | } 62 | return self; 63 | } 64 | 65 | - (instancetype)initWithConfiguration:(MXRMessageNodeConfiguration *)configuration { 66 | ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); 67 | return [self initWithImageURL:nil configuration:nil cornersToApplyMaxRadius:UIRectCornerAllCorners showsPlayButton:NO]; 68 | } 69 | 70 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 71 | ASInsetLayoutSpec* imageInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_imageNode]; 72 | if (_playButtonNode) { 73 | _playButtonNode.style.preferredSize = [MXRPlayButtonNode suggestedSizeWhenRenderedOverImageWithSizeInPoints:_imageNode.style.preferredSize]; 74 | return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imageInset overlay:[ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:_playButtonNode]]; 75 | } 76 | return imageInset; 77 | } 78 | 79 | - (void)setImageModificationBlockForSize:(CGSize)size { 80 | if (_minCornerRadius > 0.0f || _borderWidth > 0.0f) { 81 | _imageNode.imageModificationBlock = [UIImage mxr_imageModificationBlockToScaleToSize:size maximumCornerRadius:_maxCornerRadius minimumCornerRadius:_minCornerRadius borderColor:_borderColor borderWidth:_borderWidth cornersToApplyMaxRadius:_cornersHavingMaxRadius]; 82 | } 83 | } 84 | 85 | - (void)redrawBubbleWithCorners:(UIRectCorner)cornersHavingRadius { 86 | _cornersHavingMaxRadius = cornersHavingRadius; 87 | [self setImageModificationBlockForSize:_imageNode.frame.size]; 88 | [_imageNode setNeedsDisplay]; 89 | } 90 | 91 | #pragma mark - ASNetworkImageNodeDelegate 92 | 93 | - (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image { 94 | if (!image || image.size.width == 0 || image.size.height == 0) return; 95 | 96 | // The next layout pass may happen after the image finishes decoding, so we need to 97 | // give ASyncDisplayKit a hint to its target size so it decodes correctly. This is 98 | // why we set its frame. 99 | CGFloat scaleFactor = (_maxSize.width / image.size.width); 100 | CGFloat scaleFactor2 = (_maxSize.height / image.size.height); 101 | CGFloat scale = MIN(scaleFactor2, scaleFactor); 102 | 103 | CGSize newSize = CGSizeMake(image.size.width * scale, image.size.height * scale); 104 | _imageNode.frame = (CGRect){CGPointZero, newSize}; 105 | [self setImageModificationBlockForSize:newSize]; 106 | _imageNode.style.preferredSize = _imageNode.frame.size; 107 | self.style.preferredSize = _imageNode.style.preferredSize; 108 | [self setNeedsLayout]; 109 | } 110 | 111 | @end 112 | 113 | 114 | @implementation MXRMessageImageConfiguration 115 | 116 | - (instancetype)init { 117 | self = [super init]; 118 | if (self) { 119 | self.borderColor = [UIColor colorWithRed:0.592f green:0.592f blue:0.592f alpha:1]; 120 | self.borderWidth = 0.5f; 121 | _placeholderImageSize = CGSizeMake(100, 100); 122 | _maximumImageSize = [MXRMessageImageConfiguration suggestedMaxImageSizeForScreenSize:[UIScreen mainScreen].bounds.size]; 123 | } 124 | return self; 125 | } 126 | 127 | + (CGSize)suggestedMaxImageSizeForScreenSize:(CGSize)screenSize { 128 | int maximumImageWidth = (int)(screenSize.width * 0.75f); 129 | while ((maximumImageWidth % 3 != 1) && (maximumImageWidth % 2 != 0)) { 130 | // for the media collection it helps with spacing if its 1 mod 3 and 0 mod 2 131 | maximumImageWidth--; 132 | } 133 | return CGSizeMake(maximumImageWidth, 1.77f*maximumImageWidth); 134 | } 135 | 136 | @end 137 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageMediaCollectionNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageMediaCollectionNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 4/4/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageContentNode.h" 10 | 11 | #import "MXRMessengerMedium.h" 12 | 13 | @class MXRMessageMediaCollectionConfiguration; 14 | @class MXRMessageMediaCollectionNode; 15 | 16 | @protocol MXRMessageMediaCollectionNodeDelegate 17 | 18 | - (void)messageMediaCollectionNode:(MXRMessageMediaCollectionNode*)messageMediaCollectionNode didSelectMedium:(id)medium atIndexPath:(NSIndexPath*)indexPath; 19 | 20 | @end 21 | 22 | @interface MXRMessageMediaCollectionNode : MXRMessageContentNode 23 | 24 | @property (nonatomic, strong, readonly) ASCollectionNode* collectionNode; 25 | @property (nonatomic, strong, readonly) NSArray>* media; 26 | @property (nonatomic, weak) id mediaCollectionDelegate; 27 | 28 | - (instancetype)initWithMedia:(NSArray>*)media configuration:(MXRMessageMediaCollectionConfiguration*)configuration cornersToApplyMaxRadius:(UIRectCorner)cornersHavingRadius NS_DESIGNATED_INITIALIZER; 29 | 30 | @end 31 | 32 | 33 | @interface MXRMessageMediaCollectionConfiguration : MXRMessageNodeConfiguration 34 | 35 | @property (nonatomic, assign) CGFloat maxWidth; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageMediaCollectionNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageMediaCollectionNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 4/4/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageMediaCollectionNode.h" 10 | 11 | #import "UIColor+MXRMessenger.h" 12 | #import "MXRMessageMediumCellNode.h" 13 | #import "MXRMessageImageNode.h" 14 | 15 | @implementation MXRMessageMediaCollectionNode { 16 | UIRectCorner _cornersHavingRadius; 17 | CGFloat _maxWidth; 18 | CGSize _itemSize; 19 | MXRMessageMediaCollectionConfiguration* _configuration; 20 | NSInteger _topRightRow; 21 | NSInteger _bottomRightRow; 22 | NSInteger _bottomLeftRow; 23 | } 24 | 25 | - (instancetype)initWithMedia:(NSArray> *)media configuration:(MXRMessageMediaCollectionConfiguration *)configuration cornersToApplyMaxRadius:(UIRectCorner)cornersHavingRadius { 26 | self = [super initWithConfiguration:configuration]; 27 | if (self) { 28 | self.automaticallyManagesSubnodes = YES; 29 | self.userInteractionEnabled = YES; 30 | _cornersHavingRadius = cornersHavingRadius; 31 | _media = media; 32 | _maxWidth = configuration.maxWidth; 33 | _configuration = configuration; 34 | 35 | UICollectionViewFlowLayout* flowLayout = [[UICollectionViewFlowLayout alloc] init]; 36 | CGFloat spacing = 2.0f; 37 | flowLayout.minimumInteritemSpacing = spacing; 38 | flowLayout.minimumLineSpacing = spacing; 39 | flowLayout.sectionInset = UIEdgeInsetsZero; 40 | CGFloat length = 0.0f; 41 | CGFloat numRows = ceilf(media.count / 3.0f); 42 | if (media.count == 1) { 43 | length = _maxWidth; 44 | _topRightRow = _bottomLeftRow = _bottomRightRow = 0; 45 | } else if (media.count == 2) { 46 | length = floorf((_maxWidth - spacing)/2.0f); 47 | _bottomLeftRow = 0; 48 | _topRightRow = _bottomRightRow = 1; 49 | } else { 50 | length = floorf((_maxWidth - 2*spacing)/3.0f); 51 | _topRightRow = 2; 52 | _bottomLeftRow = (NSInteger)(numRows - 1)*3; 53 | _bottomRightRow = (NSInteger)(numRows - 1)*3 + 2; // may not always exist 54 | } 55 | flowLayout.itemSize = CGSizeMake(length, length); 56 | _itemSize = flowLayout.itemSize; 57 | 58 | CGFloat totalHeight = length; 59 | if (media.count > 3) { 60 | totalHeight = (numRows * (length + flowLayout.minimumLineSpacing)) - flowLayout.minimumLineSpacing; 61 | } 62 | 63 | _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:flowLayout]; 64 | _collectionNode.dataSource = self; 65 | _collectionNode.delegate = self; 66 | _collectionNode.style.preferredSize = CGSizeMake(_maxWidth, totalHeight); 67 | self.style.preferredSize = _collectionNode.style.preferredSize; 68 | } 69 | return self; 70 | } 71 | 72 | - (instancetype)initWithConfiguration:(MXRMessageNodeConfiguration *)configuration { 73 | ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); 74 | return [self initWithMedia:nil configuration:nil cornersToApplyMaxRadius:UIRectCornerAllCorners]; 75 | } 76 | 77 | - (void)didLoad { 78 | [super didLoad]; 79 | _collectionNode.view.scrollEnabled = NO; 80 | } 81 | 82 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 83 | return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_collectionNode]; 84 | } 85 | 86 | - (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section { 87 | return _media.count; 88 | } 89 | 90 | - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { 91 | CGSize itemSize = _itemSize; 92 | NSURL* imageURL = [_media[indexPath.row] mxr_messenger_imageURLForSize:itemSize]; 93 | NSURL* videoURL = [_media[indexPath.row] mxr_messenger_videoURLForSize:itemSize]; 94 | MXRMessageMediaCollectionConfiguration* configuration = _configuration; 95 | UIRectCorner cornersHavingRadius = [self cornersHavingRadiusAtRow:indexPath.row]; 96 | return ^ASCellNode*{ 97 | return [[MXRMessageMediumCellNode alloc] initWithImageURL:imageURL configuration:configuration size:itemSize cornersHavingRadius:cornersHavingRadius showsPlayButton:(videoURL != nil)]; 98 | }; 99 | } 100 | 101 | - (UIRectCorner)cornersHavingRadiusAtRow:(NSInteger)row { 102 | // 0 means none, even though it looks like UIRectCornerAllCorners 103 | UIRectCorner cornersHavingRadius = 0; 104 | if (_cornersHavingRadius & UIRectCornerTopLeft) { 105 | if (row == 0) cornersHavingRadius |= UIRectCornerTopLeft; 106 | } 107 | if (_cornersHavingRadius & UIRectCornerTopRight) { 108 | if (row == _topRightRow) cornersHavingRadius |= UIRectCornerTopRight; 109 | } 110 | if (_cornersHavingRadius & UIRectCornerBottomLeft) { 111 | if (row == _bottomLeftRow) cornersHavingRadius |= UIRectCornerBottomLeft; 112 | } 113 | if (_cornersHavingRadius & UIRectCornerBottomRight) { 114 | if (row == _bottomRightRow) cornersHavingRadius |= UIRectCornerBottomRight; 115 | } 116 | return cornersHavingRadius; 117 | } 118 | 119 | - (void)collectionNode:(ASCollectionNode *)collectionNode didSelectItemAtIndexPath:(NSIndexPath *)indexPath { 120 | [self.mediaCollectionDelegate messageMediaCollectionNode:self didSelectMedium:_media[indexPath.row] atIndexPath:indexPath]; 121 | } 122 | 123 | #pragma mark - MXRMessageContentNode 124 | 125 | - (void)redrawBubbleWithCorners:(UIRectCorner)newCornersHavingRadius { 126 | _cornersHavingRadius = newCornersHavingRadius; 127 | if (!self.isNodeLoaded) return; 128 | if (_media.count > 0) { 129 | UIRectCorner topLeft = [self cornersHavingRadiusAtRow:0]; 130 | [((MXRMessageMediumCellNode*)[_collectionNode nodeForItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]) redrawWithCornersHavingRadius:topLeft]; 131 | } 132 | if (_topRightRow != 0) { 133 | UIRectCorner topRight = [self cornersHavingRadiusAtRow:_topRightRow]; 134 | [((MXRMessageMediumCellNode*)[_collectionNode nodeForItemAtIndexPath:[NSIndexPath indexPathForRow:_topRightRow inSection:0]]) redrawWithCornersHavingRadius:topRight]; 135 | } 136 | if (_bottomLeftRow != 0) { 137 | UIRectCorner bottomLeft = [self cornersHavingRadiusAtRow:_bottomLeftRow]; 138 | [((MXRMessageMediumCellNode*)[_collectionNode nodeForItemAtIndexPath:[NSIndexPath indexPathForRow:_bottomLeftRow inSection:0]]) redrawWithCornersHavingRadius:bottomLeft]; 139 | } 140 | if ((_bottomRightRow != _topRightRow) && _bottomRightRow < _media.count) { 141 | UIRectCorner bottomRight = [self cornersHavingRadiusAtRow:_bottomRightRow]; 142 | [((MXRMessageMediumCellNode*)[_collectionNode nodeForItemAtIndexPath:[NSIndexPath indexPathForRow:_bottomRightRow inSection:0]]) redrawWithCornersHavingRadius:bottomRight]; 143 | } 144 | } 145 | 146 | @end 147 | 148 | 149 | @implementation MXRMessageMediaCollectionConfiguration 150 | 151 | - (instancetype)init { 152 | self = [super init]; 153 | if (self) { 154 | self.minCornerRadius = 3.0f; 155 | // _maxWidth should be 0 mod 2 and 1 mod 3 for best spacing results 156 | _maxWidth = [MXRMessageImageConfiguration suggestedMaxImageSizeForScreenSize:[UIScreen mainScreen].bounds.size].width; 157 | } 158 | return self; 159 | } 160 | 161 | @end 162 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageMediumCellNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageMediumCellNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 4/4/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "MXRMessageNodeConfiguration.h" 12 | #import "MXRPlayButtonNode.h" 13 | 14 | @interface MXRMessageMediumCellNode : ASCellNode 15 | 16 | @property (nonatomic, strong, readonly) ASNetworkImageNode* imageNode; 17 | @property (nonatomic, strong, readonly) MXRPlayButtonNode* playButtonNode; 18 | 19 | - (instancetype)initWithImageURL:(NSURL*)imageURL configuration:(MXRMessageNodeConfiguration*)configuration size:(CGSize)size cornersHavingRadius:(UIRectCorner)cornersHavingRadius showsPlayButton:(BOOL)showsPlayButton; 20 | 21 | - (void)redrawWithCornersHavingRadius:(UIRectCorner)cornersHavingRadius; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageMediumCellNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageMediumCellNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 4/4/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageMediumCellNode.h" 10 | 11 | #import 12 | 13 | @implementation MXRMessageMediumCellNode { 14 | MXRMessageNodeConfiguration* _configuration; 15 | CGSize _size; 16 | } 17 | 18 | - (instancetype)initWithImageURL:(NSURL *)imageURL configuration:(MXRMessageNodeConfiguration *)configuration size:(CGSize)size cornersHavingRadius:(UIRectCorner)cornersHavingRadius showsPlayButton:(BOOL)showsPlayButton { 19 | self = [super init]; 20 | if (self) { 21 | self.automaticallyManagesSubnodes = YES; 22 | self.selectionStyle = UITableViewCellSelectionStyleNone; 23 | _configuration = configuration; 24 | _size = size; 25 | 26 | ASNetworkImageNode* imageNode = nil; 27 | if (configuration.imageCache && configuration.imageDownloader) { 28 | imageNode = [[ASNetworkImageNode alloc] initWithCache:configuration.imageCache downloader:configuration.imageDownloader]; 29 | } else { 30 | imageNode = [[ASNetworkImageNode alloc] init]; 31 | } 32 | _imageNode = imageNode; 33 | imageNode.layerBacked = YES; 34 | imageNode.contentMode = UIViewContentModeScaleAspectFill; 35 | imageNode.style.preferredSize = size; 36 | [self redrawWithCornersHavingRadius:cornersHavingRadius]; 37 | imageNode.URL = imageURL; 38 | 39 | if (showsPlayButton) { 40 | _playButtonNode = [[MXRPlayButtonNode alloc] init]; 41 | _playButtonNode.style.preferredSize = [MXRPlayButtonNode suggestedSizeWhenRenderedOverImageWithSizeInPoints:size]; 42 | } 43 | } 44 | return self; 45 | } 46 | 47 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 48 | ASInsetLayoutSpec* imageInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_imageNode]; 49 | if (_playButtonNode) { 50 | return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imageInset overlay:[ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:_playButtonNode]]; 51 | } 52 | return imageInset; 53 | } 54 | 55 | - (void)redrawWithCornersHavingRadius:(UIRectCorner)cornersHavingRadius { 56 | CGFloat maxCornerRadius = cornersHavingRadius > 0 ? _configuration.maxCornerRadius : _configuration.minCornerRadius; 57 | _imageNode.imageModificationBlock = [UIImage mxr_imageModificationBlockToScaleToSize:_size maximumCornerRadius:maxCornerRadius minimumCornerRadius:_configuration.minCornerRadius borderColor:_configuration.borderColor borderWidth:_configuration.borderWidth cornersToApplyMaxRadius:cornersHavingRadius]; 58 | if (self.isNodeLoaded) [_imageNode setNeedsDisplay]; 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageNodeConfiguration.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageNodeConfiguration.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/31/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageCellConstants.h" 10 | #import 11 | 12 | @interface MXRMessageNodeConfiguration : NSObject 13 | 14 | @property (nonatomic, assign) CGFloat maxCornerRadius; 15 | @property (nonatomic, assign) CGFloat minCornerRadius; 16 | @property (nonatomic, strong) UIColor* backgroundColor; 17 | @property (nonatomic, strong) UIColor* borderColor; 18 | @property (nonatomic, assign) CGFloat borderWidth; 19 | @property (nonatomic, assign) MXRMessageMenuItemTypes menuItemTypes; 20 | @property (nonatomic, assign) BOOL showsUIMenuControllerOnLongTap; 21 | 22 | /** 23 | * Optional. Used to construct ASNetworkImageNodes. 24 | */ 25 | @property (nonatomic, strong) id imageCache; 26 | @property (nonatomic, strong) id imageDownloader; 27 | 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageNodeConfiguration.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageNodeConfiguration.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/31/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageNodeConfiguration.h" 10 | 11 | #import "UIColor+MXRMessenger.h" 12 | 13 | @implementation MXRMessageNodeConfiguration 14 | 15 | - (instancetype)init { 16 | self = [super init]; 17 | if (self) { 18 | // MXRMessageTextConfiguration calculates the maxCornerRadius based on the provided font 19 | // to create a perfect semicircle on edge. You can apply that cornerRadius to other configurations 20 | // like MXRMessageImageConfiguration so that it matches better. 21 | _maxCornerRadius = 18.0f; 22 | _minCornerRadius = 5.0f; 23 | } 24 | return self; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageTextNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageTextNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "MXRMessageNodeConfiguration.h" 12 | #import "MXRMessageContentNode.h" 13 | 14 | @class MXRMessageTextConfiguration; 15 | 16 | @interface MXRMessageTextNode : MXRMessageContentNode 17 | 18 | - (instancetype)initWithText:(NSString*)text configuration:(MXRMessageTextConfiguration*)configuration cornersToApplyMaxRadius:(UIRectCorner)cornersHavingRadius NS_DESIGNATED_INITIALIZER; 19 | 20 | @property (nonatomic, strong, readonly) ASTextNode* textNode; 21 | @property (nonatomic, strong, readonly) ASImageNode* backgroundImageNode; 22 | 23 | @end 24 | 25 | 26 | @interface MXRMessageTextConfiguration : MXRMessageNodeConfiguration 27 | 28 | @property (nonatomic, assign) UIEdgeInsets textInset; // Default: 8,12,8,12 29 | - (void)setTextInset:(UIEdgeInsets)textInset adjustMaxCornerRadiusToKeepCircular:(BOOL)adjustMaxCornerRadius; 30 | 31 | @property (nonatomic, strong, readonly) NSDictionary* textAttributes; 32 | 33 | @property (nonatomic, assign) BOOL isLinkDetectionEnabled; // Default: YES 34 | @property (nonatomic, strong) NSDictionary* linkAttributes; // Default: underlined 35 | @property (nonatomic, assign) ASTextNodeHighlightStyle linkHighlightStyle; // while touching a link 36 | 37 | - (instancetype)initWithFont:(UIFont*)font textColor:(UIColor*)textColor backgroundColor:(UIColor*)backgroundColor; 38 | - (instancetype)initWithTextAttributes:(NSDictionary*)attributes backgroundColor:(UIColor*)backgroundColor; 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRMessageTextNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessageTextNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/27/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRMessageTextNode.h" 10 | 11 | #import "UIImage+MXRMessenger.h" 12 | #import "UIColor+MXRMessenger.h" 13 | #import "MXRMessageContentNode+Subclasses.h" 14 | 15 | @interface MXRMessageTextNode () 16 | 17 | @end 18 | 19 | @implementation MXRMessageTextNode { 20 | MXRMessageTextConfiguration* _configuration; 21 | UIRectCorner _cornersHavingRadius; 22 | 23 | BOOL _delegateImplementsTapURL; 24 | BOOL _delegateImplementsLongTapURL; 25 | 26 | BOOL _hasLinks; 27 | BOOL _isTouchingURL; 28 | } 29 | 30 | - (instancetype)initWithText:(NSString *)text configuration:(MXRMessageTextConfiguration *)configuration cornersToApplyMaxRadius:(UIRectCorner)cornersHavingRadius { 31 | self = [super initWithConfiguration:configuration]; 32 | if (self) { 33 | self.automaticallyManagesSubnodes = YES; 34 | _configuration = configuration; 35 | _cornersHavingRadius = cornersHavingRadius; 36 | 37 | _textNode = [[ASTextNode alloc] init]; 38 | _textNode.layerBacked = YES; 39 | NSMutableAttributedString* attributedText = [[NSMutableAttributedString alloc] initWithString:(text ? : @"") attributes:_configuration.textAttributes]; 40 | if (_configuration.isLinkDetectionEnabled && _configuration.linkAttributes && text.length > 0) { 41 | NSArray* links = [MXRMessageTextNode applyAttributes:_configuration.linkAttributes toLinksInMutableAttributedString:attributedText]; 42 | _hasLinks = links.count > 0; 43 | _textNode.highlightStyle = _configuration.linkHighlightStyle; 44 | } 45 | _textNode.attributedText = attributedText; 46 | _backgroundImageNode = [[ASImageNode alloc] init]; 47 | _backgroundImageNode.layerBacked = YES; 48 | [self redrawBubble]; 49 | } 50 | return self; 51 | } 52 | 53 | - (instancetype)init { 54 | ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); 55 | return [self initWithText:nil configuration:nil cornersToApplyMaxRadius:UIRectCornerAllCorners]; 56 | } 57 | 58 | - (instancetype)initWithConfiguration:(MXRMessageNodeConfiguration *)configuration { 59 | ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); 60 | return [self initWithText:nil configuration:nil cornersToApplyMaxRadius:UIRectCornerAllCorners]; 61 | } 62 | 63 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 64 | ASInsetLayoutSpec* textInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:_configuration.textInset child:_textNode]; 65 | return [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:textInset background:_backgroundImageNode]; 66 | } 67 | 68 | - (void)didLoad { 69 | [super didLoad]; 70 | if (_hasLinks) { 71 | [_textNode.layer as_setAllowsHighlightDrawing:YES]; 72 | } 73 | } 74 | 75 | - (void)redrawBubble { 76 | [self redrawBubbleImageWithColor:_configuration.backgroundColor]; 77 | } 78 | 79 | - (void)redrawBubbleImageWithColor:(UIColor*)color { 80 | _backgroundImageNode.image = [UIImage mxr_bubbleImageWithMaximumCornerRadius:_configuration.maxCornerRadius minimumCornerRadius:_configuration.minCornerRadius color:color cornersToApplyMaxRadius:_cornersHavingRadius]; 81 | } 82 | 83 | #pragma mark - MXRMessageContentNode 84 | 85 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 86 | if (_hasLinks) { 87 | _isTouchingURL = [self urlTouched:touches performHighlight:YES] != nil; 88 | } 89 | [super touchesBegan:touches withEvent:event]; 90 | } 91 | 92 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { 93 | if (_isTouchingURL) { 94 | [_textNode setHighlightRange:NSMakeRange(0, 0) animated:NO]; 95 | } 96 | _isTouchingURL = NO; 97 | [super touchesCancelled:touches withEvent:event]; 98 | } 99 | 100 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 101 | [super touchesMoved:touches withEvent:event]; 102 | } 103 | 104 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 105 | if (!_isTouchingURL) { 106 | [super touchesEnded:touches withEvent:event]; 107 | return; 108 | } 109 | _isTouchingURL = NO; 110 | [_textNode setHighlightRange:NSMakeRange(0, 0) animated:NO]; 111 | NSURL* url = [self urlTouched:touches performHighlight:NO]; 112 | if (url) { 113 | CGFloat duration = event.timestamp - self.touchStartTimestamp; 114 | if (duration > 0.35f && _delegateImplementsLongTapURL) { 115 | [self.delegate messageContentNode:self didLongTapURL:url]; 116 | } else if (_delegateImplementsTapURL) { 117 | [self.delegate messageContentNode:self didTapURL:url]; 118 | } else { 119 | [[UIApplication sharedApplication] openURL:url]; 120 | } 121 | } 122 | } 123 | 124 | - (void)setHighlighted:(BOOL)highlighted { 125 | if (_isTouchingURL) { 126 | return; 127 | } 128 | BOOL didChange = highlighted != self.highlighted; 129 | [super setHighlighted:highlighted]; 130 | if (didChange) { 131 | if (highlighted) { 132 | [self redrawBubbleImageWithColor:[_configuration.backgroundColor mxr_darkerColor]]; 133 | } else { 134 | [self redrawBubbleImageWithColor:_configuration.backgroundColor]; 135 | } 136 | } 137 | } 138 | 139 | - (void)setDelegate:(id)delegate { 140 | [super setDelegate:delegate]; 141 | _delegateImplementsTapURL = [delegate respondsToSelector:@selector(messageContentNode:didTapURL:)]; 142 | _delegateImplementsLongTapURL = [delegate respondsToSelector:@selector(messageContentNode:didLongTapURL:)]; 143 | } 144 | 145 | - (void)copy:(id)sender { 146 | [[UIPasteboard generalPasteboard] setString:self.textNode.attributedText.string]; 147 | [super copy:sender]; 148 | } 149 | 150 | - (void)redrawBubbleWithCorners:(UIRectCorner)cornersHavingRadius { 151 | _cornersHavingRadius = cornersHavingRadius; 152 | [self redrawBubble]; 153 | } 154 | 155 | - (NSURL*)urlTouched:(NSSet *)touches performHighlight:(BOOL)highlight { 156 | CGPoint pointInTextNode = [self convertPoint:[[touches anyObject] locationInView:self.view] toNode:_textNode]; 157 | NSRange range = NSMakeRange(0, 0); 158 | id linkAttributeValue = [_textNode linkAttributeValueAtPoint:pointInTextNode attributeName:NULL range:&range]; 159 | if (range.length == 0 || ![linkAttributeValue isKindOfClass:[NSURL class]]) { 160 | return nil; 161 | } 162 | if (highlight) { 163 | [_textNode setHighlightRange:range animated:NO]; 164 | } 165 | return linkAttributeValue; 166 | } 167 | 168 | #pragma mark - Helper 169 | 170 | + (NSArray*)applyAttributes:(NSDictionary*)attributes toLinksInMutableAttributedString:(NSMutableAttributedString*)attributedString { 171 | NSString* text = attributedString.string; 172 | NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil]; 173 | NSArray *matches = [linkDetector matchesInString:text options:0 range:NSMakeRange(0, text.length)]; 174 | for (NSTextCheckingResult* match in matches) { 175 | if (match.range.location == NSNotFound || !match.URL) continue; 176 | NSMutableDictionary* linkAttrs = [[NSMutableDictionary alloc] initWithDictionary:attributes]; 177 | linkAttrs[NSLinkAttributeName] = match.URL; 178 | [attributedString addAttributes:linkAttrs range:match.range]; 179 | } 180 | return matches; 181 | } 182 | 183 | @end 184 | 185 | 186 | @implementation MXRMessageTextConfiguration 187 | 188 | - (instancetype)init { 189 | return [self initWithFont:[UIFont systemFontOfSize:15] textColor:[UIColor blackColor] backgroundColor:[UIColor mxr_bubbleLightGrayColor]]; 190 | } 191 | 192 | - (instancetype)initWithFont:(UIFont *)font textColor:(UIColor *)textColor backgroundColor:(UIColor *)backgroundColor { 193 | return [self initWithTextAttributes:@{NSFontAttributeName: (font ? : [UIFont systemFontOfSize:15]), NSForegroundColorAttributeName: (textColor ? : [UIColor blackColor])} backgroundColor:backgroundColor]; 194 | } 195 | 196 | - (instancetype)initWithTextAttributes:(NSDictionary *)attributes backgroundColor:(UIColor *)backgroundColor { 197 | self = [super init]; 198 | if (self) { 199 | NSMutableDictionary* attrsMutable = [(attributes ? : @{}) mutableCopy]; 200 | attrsMutable[NSFontAttributeName] = attrsMutable[NSFontAttributeName] ? : [UIFont systemFontOfSize:15]; 201 | attrsMutable[NSForegroundColorAttributeName] = attrsMutable[NSForegroundColorAttributeName] ? : [UIColor blackColor]; 202 | self.showsUIMenuControllerOnLongTap = YES; 203 | self.menuItemTypes |= MXRMessageMenuItemTypeCopy; 204 | self.backgroundColor = backgroundColor; 205 | _textAttributes = [attrsMutable copy]; 206 | _textInset = UIEdgeInsetsMake(8, 12, 8, 12); 207 | [self setTextInset:UIEdgeInsetsMake(8, 12, 8, 12) adjustMaxCornerRadiusToKeepCircular:YES]; 208 | _isLinkDetectionEnabled = YES; 209 | NSMutableDictionary* linkAttributes = [[NSMutableDictionary alloc] initWithDictionary:(_textAttributes ? : @{})]; 210 | linkAttributes[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle); 211 | linkAttributes[NSUnderlineColorAttributeName] = attributes[NSForegroundColorAttributeName]; 212 | _linkAttributes = [linkAttributes copy]; 213 | _linkHighlightStyle = ASTextNodeHighlightStyleDark; 214 | } 215 | return self; 216 | } 217 | 218 | - (void)setTextInset:(UIEdgeInsets)textInset { 219 | [self setTextInset:textInset adjustMaxCornerRadiusToKeepCircular:YES]; 220 | } 221 | 222 | - (void)setTextInset:(UIEdgeInsets)textInset adjustMaxCornerRadiusToKeepCircular:(BOOL)adjustMaxCornerRadius { 223 | _textInset = textInset; 224 | if (adjustMaxCornerRadius) { 225 | UIFont* font = self.textAttributes[NSFontAttributeName]; 226 | if (font) { 227 | self.maxCornerRadius = (ceilf(font.lineHeight) + _textInset.top + _textInset.bottom)/2.0f; 228 | } 229 | } 230 | } 231 | 232 | @end 233 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRPlayButtonNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRPlayButtonNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 4/12/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MXRPlayButtonNode : ASDisplayNode 12 | 13 | + (CGSize)suggestedSizeWhenRenderedOverImageWithSizeInPoints:(CGSize)imageSize; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /MXRMessenger/MessageCell/MXRPlayButtonNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRPlayButtonNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 4/12/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import "MXRPlayButtonNode.h" 10 | 11 | #import 12 | 13 | @implementation MXRPlayButtonNode 14 | 15 | - (instancetype)init { 16 | self = [super init]; 17 | if (self) { 18 | self.opaque = NO; 19 | } 20 | return self; 21 | } 22 | 23 | + (CGSize)suggestedSizeWhenRenderedOverImageWithSizeInPoints:(CGSize)imageSize { 24 | CGFloat smallerSide = MIN(imageSize.width, imageSize.height); 25 | if (smallerSide >= 140.0f) return CGSizeMake(70.0, 70.0f); 26 | return CGSizeMake(0.50f*smallerSide, 0.50f*smallerSide); 27 | } 28 | 29 | + (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing { 30 | 31 | UIColor *white = [UIColor whiteColor]; 32 | 33 | UIColor *fadedBlack = [[UIColor blackColor] colorWithAlphaComponent:0.6f]; 34 | CGFloat w = bounds.size.width; 35 | CGFloat h = bounds.size.height; 36 | CGFloat borderWidth = (2.5f/70.0f)*w; 37 | CGFloat tw = (22.4f/70.0f)*w; // triangle width and height 38 | CGFloat th = tw; 39 | CGFloat shift = (3.0f/70.0f)*w; 40 | 41 | CGContextRef context = UIGraphicsGetCurrentContext(); 42 | CGContextSaveGState(context); 43 | 44 | UIBezierPath* outerCirclePath = [UIBezierPath bezierPathWithOvalInRect:bounds]; 45 | [outerCirclePath addClip]; 46 | [fadedBlack setFill]; 47 | [outerCirclePath fill]; 48 | 49 | [white setStroke]; 50 | [outerCirclePath setLineWidth:borderWidth]; 51 | [outerCirclePath stroke]; 52 | 53 | UIBezierPath* trianglePath = [UIBezierPath bezierPath]; 54 | CGPoint triangleOrigin = CGPointMake((w - tw)/2.0f + shift, (h - th)/2.0f); 55 | [trianglePath moveToPoint:triangleOrigin]; 56 | [trianglePath addLineToPoint:CGPointMake((w + tw)/2.0f + shift, h/2.0f)]; 57 | [trianglePath addLineToPoint:CGPointMake((w - tw)/2.0f + shift, (h + th)/2.0f)]; 58 | [trianglePath addLineToPoint:triangleOrigin]; 59 | [trianglePath closePath]; 60 | 61 | [white setFill]; 62 | [trianglePath fill]; 63 | 64 | CGContextRestoreGState(context); 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/MXRMessengerInputToolbar.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessengerInputToolbar.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/3/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | @class MXRMessengerIconButtonNode; 14 | 15 | @interface MXRMessengerInputToolbar : ASDisplayNode 16 | 17 | @property (nonatomic, strong, readonly) MXRGrowingEditableTextNode* textInputNode; 18 | @property (nonatomic, strong) ASDisplayNode* leftButtonsNode; 19 | @property (nonatomic, strong) ASDisplayNode* rightButtonsNode; 20 | @property (nonatomic, strong, readonly) MXRMessengerIconButtonNode* defaultSendButton; // setting rightButtonsNode hides this 21 | 22 | @property (nonatomic, assign, readonly) CGFloat heightOfTextNodeWithOneLineOfText; 23 | @property (nonatomic, strong, readonly) UIFont* font; 24 | @property (nonatomic, strong, readonly) UIColor* tintColor; 25 | 26 | - (instancetype)initWithFont:(UIFont*)font placeholder:(NSString*)placeholder tintColor:(UIColor*)tintColor; 27 | 28 | - (NSString*)clearText; // clears and returns the current text with whitespace trimmed 29 | 30 | @end 31 | 32 | 33 | @interface MXRMessengerIconNode : ASDisplayNode 34 | 35 | @property (nonatomic, strong) UIColor* color; 36 | 37 | @end 38 | 39 | 40 | @interface MXRMessengerSendIconNode : MXRMessengerIconNode 41 | @end 42 | 43 | 44 | @interface MXRMessengerPlusIconNode : MXRMessengerIconNode 45 | @end 46 | 47 | 48 | @interface MXRMessengerIconButtonNode : ASControlNode 49 | 50 | @property (nonatomic, strong) MXRMessengerIconNode* icon; 51 | 52 | + (instancetype)buttonWithIcon:(MXRMessengerIconNode*)icon matchingToolbar:(MXRMessengerInputToolbar*)toolbar; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/MXRMessengerInputToolbar.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessengerInputToolbar.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/3/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | @implementation MXRMessengerInputToolbar { 14 | ASImageNode* _textInputBackgroundNode; 15 | UIEdgeInsets _textInputInsets; 16 | UIEdgeInsets _finalInsets; 17 | } 18 | 19 | - (instancetype)init { 20 | return [self initWithFont:[UIFont systemFontOfSize:16.0f] placeholder:@"Type a message" tintColor:[UIColor mxr_fbMessengerBlue]]; 21 | } 22 | 23 | - (instancetype)initWithFont:(UIFont *)font placeholder:(NSString *)placeholder tintColor:(UIColor*)tintColor { 24 | self = [super init]; 25 | if (self) { 26 | NSAssert(font, @"You forgot to provide a font to init %@", NSStringFromClass(self.class)); 27 | self.automaticallyManagesSubnodes = YES; 28 | self.backgroundColor = [UIColor whiteColor]; 29 | _font = font; 30 | _tintColor = tintColor; 31 | // #8899a6 alpha 0.85 32 | UIColor* placeholderGray = [UIColor colorWithRed:0.53 green:0.60 blue:0.65 alpha:0.85]; 33 | // #f5f8fa 34 | UIColor* veryLightGray = [UIColor colorWithRed:0.96 green:0.97 blue:0.98 alpha:1.0];; 35 | 36 | CGFloat topPadding = ceilf(0.33f * font.lineHeight); 37 | CGFloat bottomPadding = topPadding; 38 | CGFloat heightOfTextNode = ceilf(topPadding + bottomPadding + font.lineHeight); 39 | _heightOfTextNodeWithOneLineOfText = heightOfTextNode; 40 | CGFloat cornerRadius = floorf(heightOfTextNode / 2.0f); 41 | 42 | _textInputInsets = UIEdgeInsetsMake(topPadding, 0.7f*cornerRadius, bottomPadding, 0.7f*cornerRadius); 43 | 44 | _textInputBackgroundNode = [[ASImageNode alloc] init]; 45 | _textInputBackgroundNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:cornerRadius cornerColor:[UIColor whiteColor] fillColor:veryLightGray borderColor:placeholderGray borderWidth:0.5f]; 46 | _textInputBackgroundNode.displaysAsynchronously = NO; // otherwise it doesnt appear until viewDidAppear 47 | 48 | _textInputNode = [[MXRGrowingEditableTextNode alloc] init]; 49 | _textInputNode.tintColor = tintColor; 50 | _textInputNode.maximumLinesToDisplay = 6; 51 | _textInputNode.typingAttributes = @{NSFontAttributeName: font, NSForegroundColorAttributeName: [UIColor blackColor]}; 52 | NSDictionary* placeholderAttributes = @{NSFontAttributeName: font, NSForegroundColorAttributeName: placeholderGray}; 53 | _textInputNode.attributedPlaceholderText = [[NSAttributedString alloc] initWithString:(placeholder ? : @"") attributes:placeholderAttributes]; 54 | _textInputNode.style.flexGrow = 1.0f; 55 | _textInputNode.style.flexShrink = 1.0f; 56 | _textInputNode.clipsToBounds = YES; 57 | 58 | _defaultSendButton = [MXRMessengerIconButtonNode buttonWithIcon:[[MXRMessengerSendIconNode alloc] init] matchingToolbar:self]; 59 | _rightButtonsNode = _defaultSendButton; 60 | 61 | _finalInsets = UIEdgeInsetsMake(8, 0, 10, 0); 62 | } 63 | return self; 64 | } 65 | 66 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 67 | ASStackLayoutSpec* inputBar = [ASStackLayoutSpec horizontalStackLayoutSpec]; 68 | inputBar.alignItems = ASStackLayoutAlignItemsEnd; 69 | NSMutableArray* inputBarChildren = [[NSMutableArray alloc] init]; 70 | if (_leftButtonsNode) [inputBarChildren addObject:_leftButtonsNode]; 71 | 72 | ASInsetLayoutSpec* textInputInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:_textInputInsets child:_textInputNode]; 73 | ASBackgroundLayoutSpec* textInputWithBackground = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:textInputInset background:_textInputBackgroundNode]; 74 | textInputWithBackground.style.flexGrow = 1.0f; 75 | textInputWithBackground.style.flexShrink = 1.0f; 76 | if (!_leftButtonsNode) textInputWithBackground.style.spacingBefore = 8.0f; 77 | if (!_rightButtonsNode) textInputWithBackground.style.spacingAfter = 8.0f; 78 | [inputBarChildren addObject:textInputWithBackground]; 79 | 80 | if (_rightButtonsNode) [inputBarChildren addObject:_rightButtonsNode]; 81 | inputBar.children = inputBarChildren; 82 | 83 | ASInsetLayoutSpec* inputBarInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:_finalInsets child:inputBar]; 84 | return inputBarInset; 85 | } 86 | 87 | - (NSString*)clearText { 88 | NSString* text = [_textInputNode.attributedText.string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 89 | _textInputNode.attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:_textInputNode.typingAttributes]; 90 | return text; 91 | } 92 | 93 | @end 94 | 95 | 96 | @implementation MXRMessengerIconNode 97 | 98 | - (instancetype)init { 99 | self = [super init]; 100 | if (self) { 101 | self.opaque = NO; 102 | self.clipsToBounds = NO; 103 | } 104 | return self; 105 | } 106 | 107 | - (UIColor *)color { return _color ? : (_color = [UIColor blackColor]); } 108 | 109 | - (id)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { 110 | return [self color]; 111 | } 112 | 113 | @end 114 | 115 | 116 | @implementation MXRMessengerSendIconNode 117 | 118 | + (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing { 119 | 120 | CGContextRef context = UIGraphicsGetCurrentContext(); 121 | CGContextSaveGState(context); 122 | 123 | CGFloat halfStrokeWidth = 1.0f; 124 | CGFloat hsw = halfStrokeWidth; 125 | 126 | CGFloat sw = bounds.size.width / 44.0f; 127 | CGFloat sh = bounds.size.height / 44.0f; 128 | CGPoint p0 = CGPointMake(hsw, hsw); 129 | CGPoint p1 = CGPointMake(44.0f*sw - hsw, 22.0f*sh); 130 | CGPoint p2 = CGPointMake(hsw, 44.0f*sh - hsw); 131 | CGPoint p3 = CGPointMake(6.0f*sw, 27.0f*sh); 132 | CGPoint p4 = CGPointMake(32.0f*sw, 22.0f*sh); 133 | CGPoint p5 = CGPointMake(6.0f*sw, 17.0f*sh); 134 | 135 | CGPoint points[6] = { p0, p1, p2, p3, p4, p5 }; 136 | 137 | UIColor* color = (UIColor*)parameters; 138 | [color setFill]; 139 | [color setStroke]; 140 | 141 | UIBezierPath *path = [UIBezierPath bezierPath]; 142 | [path setLineWidth:2*halfStrokeWidth]; 143 | [path setLineJoinStyle:kCGLineJoinRound]; 144 | [path moveToPoint:p0]; 145 | for (int i = 1; i < 6; i++) { 146 | [path addLineToPoint:points[i]]; 147 | } 148 | [path closePath]; 149 | [path fill]; 150 | [path stroke]; 151 | 152 | CGContextRestoreGState(context); 153 | } 154 | 155 | @end 156 | 157 | 158 | @implementation MXRMessengerPlusIconNode 159 | 160 | + (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing { 161 | 162 | CGContextRef context = UIGraphicsGetCurrentContext(); 163 | CGContextSaveGState(context); 164 | 165 | UIColor* color = (UIColor*)parameters; 166 | [color setStroke]; 167 | [color setFill]; 168 | CGFloat strokeWidth = 2.0f; 169 | CGFloat halfPlusLength = ceilf(0.15f*bounds.size.width); 170 | 171 | CGRect circleContainerRect = CGRectInset(bounds, strokeWidth/2.0f, strokeWidth/2.0f); 172 | UIBezierPath* circlePath = [UIBezierPath bezierPathWithOvalInRect:circleContainerRect]; 173 | [circlePath setLineWidth:strokeWidth]; 174 | [circlePath stroke]; 175 | [circlePath fill]; 176 | 177 | UIBezierPath* plusPath = [UIBezierPath bezierPath]; 178 | [plusPath setLineWidth:strokeWidth]; 179 | [plusPath setLineCapStyle:kCGLineCapRound]; 180 | CGFloat centerX = CGRectGetMidX(bounds); 181 | CGFloat centerY = CGRectGetMidY(bounds); 182 | 183 | [plusPath moveToPoint:CGPointMake(centerX, centerY - halfPlusLength)]; 184 | [plusPath addLineToPoint:CGPointMake(centerX, centerY + halfPlusLength)]; 185 | [plusPath moveToPoint:CGPointMake(centerX - halfPlusLength, centerY)]; 186 | [plusPath addLineToPoint:CGPointMake(centerX + halfPlusLength, centerY)]; 187 | 188 | [[UIColor whiteColor] setStroke]; 189 | [plusPath stroke]; 190 | 191 | CGContextRestoreGState(context); 192 | } 193 | 194 | @end 195 | 196 | 197 | @implementation MXRMessengerIconButtonNode 198 | 199 | - (instancetype)init { 200 | self = [super init]; 201 | if (self) { 202 | self.automaticallyManagesSubnodes = YES; 203 | } 204 | return self; 205 | } 206 | 207 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 208 | return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:_icon]; 209 | } 210 | 211 | 212 | + (instancetype)buttonWithIcon:(MXRMessengerIconNode *)icon matchingToolbar:(MXRMessengerInputToolbar *)toolbar { 213 | MXRMessengerIconButtonNode* button = [[MXRMessengerIconButtonNode alloc] init]; 214 | button.icon = icon; 215 | icon.displaysAsynchronously = NO; // otherwise it doesnt appear until viewDidAppear 216 | button.displaysAsynchronously = NO; 217 | icon.color = toolbar.tintColor; 218 | CGFloat iconWidth = ceilf(toolbar.font.lineHeight) + 2.0f; 219 | icon.style.preferredSize = CGSizeMake(iconWidth, iconWidth); 220 | button.style.preferredSize = CGSizeMake(iconWidth + 22.0f, toolbar.heightOfTextNodeWithOneLineOfText); 221 | button.hitTestSlop = UIEdgeInsetsMake(-4.0f, 0, -10.0f, 0.0f); 222 | return button; 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/MXRMessengerNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessengerNode.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/22/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MXRMessengerNode : ASDisplayNode 12 | 13 | @property (nonatomic, strong, readonly) ASTableNode* tableNode; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/MXRMessengerNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessengerNode.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/22/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @implementation MXRMessengerNode 12 | 13 | - (instancetype)init { 14 | self = [super init]; 15 | if (self) { 16 | self.automaticallyManagesSubnodes = YES; 17 | self.backgroundColor = [UIColor whiteColor]; 18 | 19 | _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; 20 | _tableNode.inverted = YES; 21 | } 22 | return self; 23 | } 24 | 25 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 26 | return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_tableNode]; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/MXRMessengerViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessengerViewController.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/22/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | 15 | @interface MXRMessengerViewController : ASViewController 16 | 17 | @property (nonatomic, strong, readonly) MXRMessengerInputToolbar* toolbar; 18 | 19 | - (instancetype)initWithNode:(MXRMessengerNode *)node toolbar:(MXRMessengerInputToolbar*)toolbar NS_DESIGNATED_INITIALIZER; 20 | - (instancetype)initWithToolbar:(MXRMessengerInputToolbar*)toolbar; 21 | 22 | /** 23 | * Override to provide custom top inset if the content spills under some view at the top. 24 | * This may need some playing with because of iOS 11 and iPhone X. 25 | * Defaults to calculating the height of status and nav bars + 6. 26 | */ 27 | - (CGFloat)calculateTopInset; 28 | 29 | - (void)dismissKeyboard; 30 | 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/MXRMessengerViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MXRMessengerViewController.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 2/22/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | @interface MXRMessengerViewController () 14 | 15 | @property (nonatomic, strong) MXRMessengerInputToolbarContainerView* toolbarContainerView; 16 | @property (nonatomic, strong) NSNumber* calculatedOffsetFromInteractiveKeyboardDismissal; 17 | @property (nonatomic, assign) CGFloat minimumBottomInset; 18 | @property (nonatomic, assign) CGFloat topInset; 19 | 20 | @end 21 | 22 | @implementation MXRMessengerViewController 23 | 24 | - (instancetype)init { 25 | return [self initWithNode:[[MXRMessengerNode alloc] init] toolbar:[[MXRMessengerInputToolbar alloc] init]]; 26 | } 27 | 28 | - (instancetype)initWithToolbar:(MXRMessengerInputToolbar *)toolbar { 29 | return [self initWithNode:[[MXRMessengerNode alloc] init] toolbar:toolbar]; 30 | } 31 | 32 | - (instancetype)initWithNode:(ASDisplayNode *)node { 33 | NSAssert(NO, @"You did not call the designated initializer of %@", NSStringFromClass([self class])); 34 | return [self initWithNode:[[MXRMessengerNode alloc] init] toolbar:[[MXRMessengerInputToolbar alloc] init]]; 35 | } 36 | 37 | - (instancetype)initWithNode:(MXRMessengerNode *)node toolbar:(MXRMessengerInputToolbar *)toolbar { 38 | self = [super initWithNode:node]; 39 | if (self) { 40 | self.hidesBottomBarWhenPushed = YES; 41 | _toolbar = toolbar; 42 | } 43 | return self; 44 | } 45 | 46 | - (void)viewDidLoad { 47 | [super viewDidLoad]; 48 | if (@available(iOS 11, *)) { 49 | self.node.tableNode.view.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 50 | } else { 51 | self.automaticallyAdjustsScrollViewInsets = NO; 52 | } 53 | 54 | CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; 55 | _toolbarContainerView = [[MXRMessengerInputToolbarContainerView alloc] initWithMessengerInputToolbar:self.toolbar constrainedSize:ASSizeRangeMake(CGSizeMake(screenWidth, 0), CGSizeMake(screenWidth, CGFLOAT_MAX))]; 56 | _minimumBottomInset = self.toolbarContainerView.toolbarNode.calculatedSize.height; 57 | _topInset = [self calculateTopInset]; 58 | 59 | self.node.tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; 60 | self.node.tableNode.view.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; 61 | self.node.tableNode.contentInset = UIEdgeInsetsMake(_minimumBottomInset, 0, _topInset, 0); 62 | self.node.tableNode.view.scrollIndicatorInsets = UIEdgeInsetsMake(_minimumBottomInset, 0, _topInset, 0); 63 | self.node.tableNode.delegate = self; 64 | 65 | [self observeKeyboardChanges]; 66 | [self observeAppStateChanges]; 67 | } 68 | 69 | - (void)viewWillDisappear:(BOOL)animated { 70 | [super viewWillDisappear:animated]; 71 | [self dismissKeyboard]; 72 | } 73 | 74 | - (void)viewDidDisappear:(BOOL)animated { 75 | [super viewDidDisappear:animated]; 76 | [self dismissKeyboard]; 77 | } 78 | 79 | - (void)viewWillAppear:(BOOL)animated { 80 | [super viewWillAppear:animated]; 81 | [self becomeFirstResponder]; 82 | } 83 | 84 | - (void)dealloc { 85 | [self stopObservingKeyboard]; 86 | [self stopObservingAppStateChanges]; 87 | } 88 | 89 | - (CGFloat)calculateTopInset { 90 | CGFloat t = 6.0f; 91 | if (!self.prefersStatusBarHidden) { 92 | t += [UIApplication sharedApplication].statusBarFrame.size.height; 93 | } 94 | if (self.navigationController && !self.navigationController.isNavigationBarHidden) { 95 | t += self.navigationController.navigationBar.frame.size.height; 96 | } 97 | return t; 98 | } 99 | 100 | #pragma mark - NSNotificationCenter 101 | 102 | - (void)observeKeyboardChanges { 103 | [self stopObservingKeyboard]; 104 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mxr_messenger_didReceiveKeyboardWillChangeFrameNotification:) name:UIKeyboardWillChangeFrameNotification object:nil]; 105 | } 106 | 107 | - (void)stopObservingKeyboard { 108 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; 109 | } 110 | 111 | - (void)observeAppStateChanges { 112 | [self stopObservingAppStateChanges]; 113 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mxr_messenger_applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; 114 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mxr_messenger_applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; 115 | } 116 | 117 | - (void)stopObservingAppStateChanges { 118 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; 119 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; 120 | } 121 | 122 | #pragma mark - Target-Action AppState 123 | 124 | - (void)mxr_messenger_applicationWillResignActive:(id)sender { 125 | [self stopObservingKeyboard]; 126 | } 127 | 128 | - (void)mxr_messenger_applicationDidBecomeActive:(id)sender { 129 | [self observeKeyboardChanges]; 130 | } 131 | 132 | #pragma mark - Target-Action Keyboard 133 | 134 | - (void)mxr_messenger_didReceiveKeyboardWillChangeFrameNotification:(NSNotification*)notification { 135 | if (self.isBeingDismissed || (self.navigationController && self.navigationController.topViewController != self)) { 136 | return; 137 | } 138 | UITableView* tableView = self.node.tableNode.view; 139 | CGFloat keyboardEndHeight = [UIScreen mainScreen].bounds.size.height - [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].origin.y; 140 | CGFloat keyboardStartHeight = tableView.contentInset.top; // this is more reliable than the startFrame in userInfo 141 | CGFloat changeInHeight = keyboardEndHeight - keyboardStartHeight; 142 | if (changeInHeight == 0) return; // e.g. when an interactive dismiss is cancelled 143 | if (keyboardEndHeight < self.minimumBottomInset) return; // e.g. when we present media viewer, it dismisses the toolbar 144 | BOOL willDismissKeyboard = changeInHeight < 0; 145 | CGFloat newOffset = tableView.contentOffset.y - changeInHeight; 146 | CGFloat offsetAtBottom = -keyboardEndHeight; 147 | if (fabs(newOffset - offsetAtBottom) < 400.0f) { 148 | newOffset = offsetAtBottom; // keep them on the most recent message when they're near it 149 | } 150 | if (tableView.isDragging && willDismissKeyboard) { 151 | self.calculatedOffsetFromInteractiveKeyboardDismissal = @(newOffset); 152 | } else { 153 | tableView.contentOffset = CGPointMake(0, newOffset); 154 | } 155 | tableView.contentInset = UIEdgeInsetsMake(keyboardEndHeight, 0, self.topInset, 0); 156 | tableView.scrollIndicatorInsets = UIEdgeInsetsMake(keyboardEndHeight, 0, self.topInset, 0); 157 | } 158 | 159 | - (void)dismissKeyboard { 160 | if ([self.toolbar.textInputNode isFirstResponder]) [self.toolbar.textInputNode resignFirstResponder]; 161 | } 162 | 163 | #pragma mark - ASTableDelegate 164 | 165 | - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { 166 | if (self.calculatedOffsetFromInteractiveKeyboardDismissal) { 167 | *targetContentOffset = CGPointMake(0, self.calculatedOffsetFromInteractiveKeyboardDismissal.doubleValue); 168 | self.calculatedOffsetFromInteractiveKeyboardDismissal = nil; 169 | } 170 | } 171 | 172 | #pragma mark - Toolbar 173 | 174 | - (BOOL)canBecomeFirstResponder { return YES; } 175 | - (UIView *)inputAccessoryView { return self.toolbarContainerView; } 176 | 177 | 178 | @end 179 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/_MXRMessengerInputToolbarContainerView.h: -------------------------------------------------------------------------------- 1 | // 2 | // _MXRMessengerInputToolbarContainerView.h 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/18/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | /** 14 | * This container view is meant to be returned as an inputAccessoryView 15 | * http://stackoverflow.com/questions/25816994/changing-the-frame-of-an-inputaccessoryview-in-ios-8 16 | */ 17 | @interface MXRMessengerInputToolbarContainerView : UIView 18 | 19 | @property (nonatomic, strong, readonly) MXRMessengerInputToolbar* toolbarNode; 20 | 21 | - (instancetype)initWithMessengerInputToolbar:(MXRMessengerInputToolbar *)toolbarNode constrainedSize:(ASSizeRange)constrainedSize; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /MXRMessenger/ViewController/_MXRMessengerInputToolbarContainerView.m: -------------------------------------------------------------------------------- 1 | // 2 | // _MXRMessengerInputToolbarContainerView.m 3 | // Mixer 4 | // 5 | // Created by Scott Kensell on 3/18/17. 6 | // Copyright © 2017 Two To Tango. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | static NSString* MXRNewCalculatedSizeNotification = @"MXRNewCalculatedSizeNotification"; 12 | 13 | @interface MXRNewSizeNotifyingNode : ASDisplayNode 14 | 15 | @property (nonatomic, strong) ASDisplayNode* innerNode; 16 | 17 | @end 18 | 19 | @implementation MXRMessengerInputToolbarContainerView { 20 | id _newSizeNotification; 21 | MXRNewSizeNotifyingNode* _containerNode; 22 | } 23 | 24 | - (instancetype)initWithMessengerInputToolbar:(MXRMessengerInputToolbar *)toolbarNode constrainedSize:(ASSizeRange)constrainedSize { 25 | self = [super initWithFrame:CGRectZero]; 26 | if (self) { 27 | _toolbarNode = toolbarNode; 28 | _containerNode = [[MXRNewSizeNotifyingNode alloc] init]; 29 | _containerNode.innerNode = _toolbarNode; 30 | [_containerNode layoutThatFits:constrainedSize]; 31 | _containerNode.frame = CGRectMake(0, 0, _containerNode.calculatedSize.width, _containerNode.calculatedSize.height); 32 | self.frame = _containerNode.frame; 33 | self.autoresizingMask = UIViewAutoresizingFlexibleHeight; // NOTE: Do not set flexibleHeight on Node too, creates bugs. 34 | 35 | CALayer* iphoneXHackyLayer = [[CALayer alloc] init]; 36 | iphoneXHackyLayer.backgroundColor = [toolbarNode.backgroundColor CGColor]; 37 | iphoneXHackyLayer.frame = CGRectMake(0, 0, self.frame.size.width, 10*self.frame.size.height); 38 | [self.layer addSublayer:iphoneXHackyLayer]; 39 | 40 | [self addSubview:_containerNode.view]; 41 | 42 | __weak typeof(self) weakSelf = self; 43 | _newSizeNotification = [[NSNotificationCenter defaultCenter] addObserverForName:MXRNewCalculatedSizeNotification object:_containerNode queue:nil usingBlock:^(NSNotification * _Nonnull note) { 44 | [weakSelf invalidateIntrinsicContentSize]; 45 | }]; 46 | } 47 | return self; 48 | } 49 | 50 | - (void)didMoveToWindow { 51 | [super didMoveToWindow]; 52 | if (@available(iOS 11, *)) { 53 | NSLayoutYAxisAnchor* systemBottomAnchor = self.window.safeAreaLayoutGuide.bottomAnchor; 54 | if (systemBottomAnchor) { 55 | [self.bottomAnchor constraintLessThanOrEqualToSystemSpacingBelowAnchor:systemBottomAnchor multiplier:1.0f].active = YES; 56 | } 57 | } 58 | } 59 | 60 | - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:_newSizeNotification]; } 61 | - (CGSize)intrinsicContentSize { return _containerNode.calculatedSize; } 62 | 63 | @end 64 | 65 | 66 | @implementation MXRNewSizeNotifyingNode { 67 | CGSize _previouslyCalculatedSize; 68 | } 69 | 70 | - (instancetype)init { 71 | self = [super init]; 72 | if (self) { 73 | self.automaticallyManagesSubnodes = YES; 74 | } 75 | return self; 76 | } 77 | 78 | - (void)calculatedLayoutDidChange { 79 | [super calculatedLayoutDidChange]; 80 | if (!CGSizeEqualToSize(_previouslyCalculatedSize, self.calculatedSize)) { 81 | _previouslyCalculatedSize = self.calculatedSize; 82 | [[NSNotificationCenter defaultCenter] postNotificationName:MXRNewCalculatedSizeNotification object:self]; 83 | } 84 | } 85 | 86 | - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { 87 | return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_innerNode]; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MXRMessenger 2 | 3 | 4 | [![Version](https://img.shields.io/cocoapods/v/MXRMessenger.svg?style=flat)](http://cocoapods.org/pods/MXRMessenger) 5 | [![License](https://img.shields.io/cocoapods/l/MXRMessenger.svg?style=flat)](http://cocoapods.org/pods/MXRMessenger) 6 | [![Platform](https://img.shields.io/cocoapods/p/MXRMessenger.svg?style=flat)](http://cocoapods.org/pods/MXRMessenger) 7 | 8 | ## Why Another Chat Library? 9 | 10 | MXRMessenger is a customizable chat library meant to provide a smooth-scrolling, responsive experience. I felt the need to write it because 11 | 12 | - [NMessenger](https://github.com/eBay/NMessenger) is Swift-only: see https://github.com/eBay/NMessenger/issues/40 13 | - [JSQMessagesViewController](https://github.com/jessesquires/JSQMessagesViewController) is UIKit-based and was the library we were using until we experienced stability and performance issues. It is also no longer being maintained. 14 | - I could not find another [Texture](http://texturegroup.org) (or ASDK) chat library which was lightweight and customizable enough. Sometimes they depend on [SlackTextViewController](https://github.com/slackhq/slacktextviewcontroller#core) which is a good library, but I thought probably overkill for our needs. 15 | 16 | That said, if you have never worked with [Texture](http://texturegroup.org), you are probably better off choosing one of the more mature libraries linked above. This library is also hardly tested and used by only 1 app so far in production, so use at your own risk. 17 | 18 | ## Features 19 | 20 |
21 | Screenshot 1 22 | Screenshot 2 23 |
24 | 25 | The `MXRMessengerViewController` is like a baby version of the [SlackTextViewController](https://github.com/slackhq/slacktextviewcontroller#core) and can be included on its own with `pod 'MXRMessenger/ViewController'`. It features 26 | 27 | - An input toolbar which you can dismiss interactively. 28 | - A growing input text node with a max number of lines. 29 | - Customizable fonts, colors, and buttons. 30 | 31 | The other subspec `pod 'MXRMessenger/MessageCell'` provides Facebook-style bubbles and a friendly factory. Main features: 32 | 33 | - Send text, image, video, or multiple images/video at the same time. 34 | - Rounded corners which dynamically update to group messages from the same sender. 35 | - Automatic date formatting. 36 | - Images dynamically update their size to true aspect ratio when sent 1 at a time. 37 | - Copy, Delete, and easy opt-in menu items. 38 | - Tap/gesture callbacks. 39 | - Fully customizable, whether it's left-to-right, no avatar, spacing issues, font or color issues, a lot is customizable. 40 | 41 | And since we are dependent on [Texture](http://texturegroup.org), you get 60 fps smoothness for free. 42 | 43 | ## Installation 44 | 45 | MXRMessenger is available through [CocoaPods](http://cocoapods.org). 46 | 47 | For the full chat library, add `pod 'MXRMessenger'` to your Podfile. 48 | 49 | For just the ViewController add `pod 'MXRMessenger/ViewController'`. 50 | 51 | For just the Facebook-style bubbles add `pod 'MXRMessenger/MessageCell'`. 52 | 53 | Where necessary, add 54 | 55 | ```Obj-C 56 | #import 57 | ``` 58 | 59 | ## Use 60 | 61 | Subclass `MXRMessengerViewController`. Then, the easiest way to instantiate message cells is with the provided `MXRMessageCellFactory`. So create a strong property: 62 | 63 | ```Obj-C 64 | @property (nonatomic, strong) MXRMessageCellFactory* cellFactory; 65 | ``` 66 | 67 | Most customizations happen at init. You can copy-paste the following code and just remove or customize whatever you want. And then implement the delegate or datasource methods the compiler complains about. 68 | 69 | ```Obj-C 70 | - (instancetype)init { 71 | MXRMessengerInputToolbar* toolbar = [[MXRMessengerInputToolbar alloc] initWithFont:[UIFont systemFontOfSize:16.0f] placeholder:@"Type a message" tintColor:[UIColor mxr_fbMessengerBlue]]; 72 | self = [super initWithToolbar:toolbar]; 73 | if (self) { 74 | // add extra buttons to toolbar 75 | MXRMessengerIconButtonNode* addPhotosBarButtonButtonNode = [MXRMessengerIconButtonNode buttonWithIcon:[[MXRMessengerPlusIconNode alloc] init] matchingToolbar:self.toolbar]; 76 | [addPhotosBarButtonButtonNode addTarget:self action:@selector(tapAddPhotos:) forControlEvents:ASControlNodeEventTouchUpInside]; 77 | self.toolbar.leftButtonsNode = addPhotosBarButtonButtonNode; 78 | [self.toolbar.defaultSendButton addTarget:self action:@selector(tapSend:) forControlEvents:ASControlNodeEventTouchUpInside]; 79 | 80 | // delegate must be self for interactive keyboard, datasource can be whatever 81 | self.node.tableNode.delegate = self; 82 | self.node.tableNode.dataSource = self; 83 | [self customizeCellFactory]; 84 | } 85 | return self 86 | } 87 | 88 | - (void)customizeCellFactory { 89 | MXRMessageCellLayoutConfiguration* layoutConfigForMe = [MXRMessageCellLayoutConfiguration rightToLeft]; 90 | MXRMessageCellLayoutConfiguration* layoutConfigForOthers = [MXRMessageCellLayoutConfiguration leftToRight]; 91 | 92 | MXRMessageAvatarConfiguration* avatarConfigForMe = nil; 93 | MXRMessageAvatarConfiguration* avatarConfigForOthers = [[MXRMessageAvatarConfiguration alloc] init]; 94 | 95 | MXRMessageTextConfiguration* textConfigForMe = [[MXRMessageTextConfiguration alloc] initWithFont:nil textColor:[UIColor whiteColor] backgroundColor:[UIColor mxr_fbMessengerBlue]]; 96 | MXRMessageTextConfiguration* textConfigForOthers = [[MXRMessageTextConfiguration alloc] initWithFont:nil textColor:[UIColor blackColor] backgroundColor:[UIColor mxr_bubbleLightGrayColor]]; 97 | CGFloat maxCornerRadius = textConfigForMe.maxCornerRadius; 98 | 99 | MXRMessageImageConfiguration* imageConfig = [[MXRMessageImageConfiguration alloc] init]; 100 | imageConfig.maxCornerRadius = maxCornerRadius; 101 | MXRMessageMediaCollectionConfiguration* mediaCollectionConfig = [[MXRMessageMediaCollectionConfiguration alloc] init]; 102 | mediaCollectionConfig.maxCornerRadius = maxCornerRadius; 103 | 104 | textConfigForMe.menuItemTypes |= MXRMessageMenuItemTypeDelete; 105 | textConfigForOthers.menuItemTypes |= MXRMessageMenuItemTypeDelete; 106 | imageConfig.menuItemTypes |= MXRMessageMenuItemTypeDelete; 107 | imageConfig.showsUIMenuControllerOnLongTap = YES; 108 | CGFloat s = [UIScreen mainScreen].scale; 109 | imageConfig.borderWidth = s > 0 ? (1.0f/s) : 0.5f; 110 | 111 | MXRMessageCellConfiguration* cellConfigForMe = [[MXRMessageCellConfiguration alloc] initWithLayoutConfig:layoutConfigForMe avatarConfig:avatarConfigForMe textConfig:textConfigForMe imageConfig:imageConfig mediaCollectionConfig:mediaCollectionConfig]; 112 | MXRMessageCellConfiguration* cellConfigForOthers = [[MXRMessageCellConfiguration alloc] initWithLayoutConfig:layoutConfigForOthers avatarConfig:avatarConfigForOthers textConfig:textConfigForOthers imageConfig:imageConfig mediaCollectionConfig:mediaCollectionConfig]; 113 | 114 | self.cellFactory = [[MXRMessageCellFactory alloc] initWithCellConfigForMe:cellConfigForMe cellConfigForOthers:cellConfigForOthers]; 115 | self.cellFactory.dataSource = self; 116 | self.cellFactory.contentNodeDelegate = self; 117 | self.cellFactory.mediaCollectionDelegate = self; 118 | } 119 | 120 | ``` 121 | 122 | To create the message cells, you can implement the `ASTableDataSource` method like so 123 | ```Obj-C 124 | - (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { 125 | Message* message = self.messages[indexPath.row]; 126 | if (message.media.count > 1) { 127 | return [self.cellFactory cellNodeBlockWithMedia:message.media tableNode:tableNode row:indexPath.row]; 128 | } else if (message.media.count == 1) { 129 | MessageMedium* medium = message.media.firstObject; 130 | return [self.cellFactory cellNodeBlockWithImageURL:medium.photoURL showsPlayButton:(medium.videoURL != nil) tableNode:tableNode row:indexPath.row]; 131 | } else { 132 | return [self.cellFactory cellNodeBlockWithText:message.text tableNode:tableNode row:indexPath.row]; 133 | } 134 | } 135 | ``` 136 | and to send a new message or update the table with new/old messages there is just one method on the `cellFactory`: 137 | ```Obj-C 138 | - (void)updateTableNode:(ASTableNode*)tableNode animated:(BOOL)animated withInsertions:(NSArray*)insertions deletions:(NSArray*)deletions reloads:(NSArray*)reloads completion:(void(^)(BOOL))completion; 139 | 140 | ``` 141 | 142 | 143 | There are a few key assumptions to keep in mind: 144 | 145 | - There is only one section: 0. 146 | - The table is [inverted](http://texturegroup.org/docs/inversion.html) so insert new messages at (0,0). 147 | - Timestamp headers are automatically shown for messages with a gap greater than 15 minutes, but you can disable this if you want. You can show/hide them programmatically through a method on the `cellFactory` (e.g. when the user taps the cell). 148 | - Corner-rounding management happens by calling the `MXRMessageCellFactoryDataSource` methods for the current indexPath as well as its neighbors. That means you should keep those methods quick - ideally they are just dictionary lookups. 149 | - Showing media on tap is outside the scope of this library. I have a Texture-based media viewer which I use and will hopefully open source some day. 150 | 151 | For more details, it's probably best to check the example project. 152 | 153 | 154 | ## Example 155 | 156 | To run the example project, clone the repo, and run `pod install` from the Example directory which has a `Podfile`. Then open the `*.xcworkspace` file and build. 157 | 158 | ## Requirements 159 | 160 | Texture 2.X and iOS 8+. 161 | 162 | ## Contributing 163 | 164 | There are no tests at present. Keep PRs very small please. 165 | 166 | ## Author 167 | 168 | Scott Kensell, skensell@gmail.com 169 | 170 | ## License 171 | 172 | MXRMessenger is available under the MIT license. See the LICENSE file for more info. 173 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------