├── .gitignore ├── README.md ├── build └── message-indicator.zip ├── message-indicator.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ ├── abeals.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── alexbeals.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings └── xcuserdata │ ├── abeals.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ └── alexbeals.xcuserdatad │ └── xcschemes │ ├── message-indicator.xcscheme │ └── xcschememanagement.plist ├── message-indicator ├── Info.plist ├── ZKSwizzle.h ├── ZKSwizzle.m └── messageIndicator.m └── preview.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Message Indicator 2 | 3 |

4 | 5 |

6 | 7 | ### The Problem 8 | I commonly open chats in Messages on my computer to make the 'unread' indicator go away. However, if there's any delay in answering them back, it's quite common that I'll simply forget to respond to the text for a while. 9 | 10 | ### The Solution 11 | I read about SIMBL, a plugin manager for Mac OS X, a while ago in pursuing a different project. Effectively, it acts much like Cydia does for iOS, but for your Mac instead. I used it to build a bundle that inserts itself into the Messages application, and adds a small gray indicator icon on chats in which you were not the last person to respond (it doesn't show up on group chats, as it quickly becomes unwieldy in large chats). This makes it a lot easier to see the messages that you haven't responded back to. I've only tested this on 10.12, but it should conceivably work on earlier versions. 12 | 13 | To clear an indicator, merely Ctrl-Click (right-click) on it. This will clear it until the next message in the chat. 14 | 15 | ## Installation: 16 | 1. Download [mySIMBL](https://github.com/w0lfschild/app_updates/raw/master/mySIMBL/mySIMBL_0.2.5.zip). 17 | 2. Download [Message Indicator](https://github.com/dado3212/message-indicator/raw/master/build/message-indicator.zip). 18 | 3. Unzip the downloaded .zip file. 19 | 4. Open `message-indicator.bundle` with `mySIMBL.app`, or simply drag and drop it. 20 | 5. Restart Messages. 21 | -------------------------------------------------------------------------------- /build/message-indicator.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dado3212/message-indicator/c4961e3e72a558f5ec1eb8385f6678e93b1ecb8e/build/message-indicator.zip -------------------------------------------------------------------------------- /message-indicator.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C398D84715BD8B0700543187 /* messageIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = C398D84615BD8B0700543187 /* messageIndicator.m */; }; 11 | FBAE21EE1CFCCA4F007E5A8E /* ZKSwizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = FBAE21ED1CFCCA4F007E5A8E /* ZKSwizzle.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 8D5B49B6048680CD000E48DA /* message-indicator.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "message-indicator.bundle"; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = "message-indicator/Info.plist"; sourceTree = ""; }; 17 | C398D84615BD8B0700543187 /* messageIndicator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = messageIndicator.m; path = "message-indicator/messageIndicator.m"; sourceTree = ""; }; 18 | FBAE21EC1CFCCA4F007E5A8E /* ZKSwizzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ZKSwizzle.h; path = "message-indicator/ZKSwizzle.h"; sourceTree = ""; }; 19 | FBAE21ED1CFCCA4F007E5A8E /* ZKSwizzle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ZKSwizzle.m; path = "message-indicator/ZKSwizzle.m"; sourceTree = ""; }; 20 | /* End PBXFileReference section */ 21 | 22 | /* Begin PBXFrameworksBuildPhase section */ 23 | 8D5B49B3048680CD000E48DA /* Frameworks */ = { 24 | isa = PBXFrameworksBuildPhase; 25 | buildActionMask = 2147483647; 26 | files = ( 27 | ); 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXFrameworksBuildPhase section */ 31 | 32 | /* Begin PBXGroup section */ 33 | 089C166AFE841209C02AAC07 /* ColorfulSidebar */ = { 34 | isa = PBXGroup; 35 | children = ( 36 | 08FB77AFFE84173DC02AAC07 /* Classes */, 37 | 089C167CFE841241C02AAC07 /* Resources */, 38 | 19C28FB8FE9D52D311CA2CBB /* Products */, 39 | ); 40 | name = ColorfulSidebar; 41 | sourceTree = ""; 42 | }; 43 | 089C167CFE841241C02AAC07 /* Resources */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | 8D5B49B7048680CD000E48DA /* Info.plist */, 47 | ); 48 | name = Resources; 49 | sourceTree = ""; 50 | }; 51 | 08FB77AFFE84173DC02AAC07 /* Classes */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | FBAE21EC1CFCCA4F007E5A8E /* ZKSwizzle.h */, 55 | FBAE21ED1CFCCA4F007E5A8E /* ZKSwizzle.m */, 56 | C398D84615BD8B0700543187 /* messageIndicator.m */, 57 | ); 58 | name = Classes; 59 | sourceTree = ""; 60 | }; 61 | 19C28FB8FE9D52D311CA2CBB /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 8D5B49B6048680CD000E48DA /* message-indicator.bundle */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | /* End PBXGroup section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | 8D5B49AC048680CD000E48DA /* message-indicator */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = 1DEB913A08733D840010E9CD /* Build configuration list for PBXNativeTarget "message-indicator" */; 75 | buildPhases = ( 76 | 8D5B49AF048680CD000E48DA /* Resources */, 77 | 8D5B49B1048680CD000E48DA /* Sources */, 78 | 8D5B49B3048680CD000E48DA /* Frameworks */, 79 | ); 80 | buildRules = ( 81 | ); 82 | dependencies = ( 83 | ); 84 | name = "message-indicator"; 85 | productInstallPath = "$(HOME)/Library/Bundles"; 86 | productName = ColorfulSidebar; 87 | productReference = 8D5B49B6048680CD000E48DA /* message-indicator.bundle */; 88 | productType = "com.apple.product-type.bundle"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 089C1669FE841209C02AAC07 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastUpgradeCheck = 0810; 97 | ORGANIZATIONNAME = cvz.; 98 | }; 99 | buildConfigurationList = 1DEB913E08733D840010E9CD /* Build configuration list for PBXProject "message-indicator" */; 100 | compatibilityVersion = "Xcode 3.2"; 101 | developmentRegion = English; 102 | hasScannedForEncodings = 1; 103 | knownRegions = ( 104 | en, 105 | ); 106 | mainGroup = 089C166AFE841209C02AAC07 /* ColorfulSidebar */; 107 | projectDirPath = ""; 108 | projectRoot = ""; 109 | targets = ( 110 | 8D5B49AC048680CD000E48DA /* message-indicator */, 111 | ); 112 | }; 113 | /* End PBXProject section */ 114 | 115 | /* Begin PBXResourcesBuildPhase section */ 116 | 8D5B49AF048680CD000E48DA /* Resources */ = { 117 | isa = PBXResourcesBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | ); 121 | runOnlyForDeploymentPostprocessing = 0; 122 | }; 123 | /* End PBXResourcesBuildPhase section */ 124 | 125 | /* Begin PBXSourcesBuildPhase section */ 126 | 8D5B49B1048680CD000E48DA /* Sources */ = { 127 | isa = PBXSourcesBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | C398D84715BD8B0700543187 /* messageIndicator.m in Sources */, 131 | FBAE21EE1CFCCA4F007E5A8E /* ZKSwizzle.m in Sources */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXSourcesBuildPhase section */ 136 | 137 | /* Begin XCBuildConfiguration section */ 138 | 1DEB913B08733D840010E9CD /* Debug */ = { 139 | isa = XCBuildConfiguration; 140 | buildSettings = { 141 | ALWAYS_SEARCH_USER_PATHS = NO; 142 | COMBINE_HIDPI_IMAGES = YES; 143 | COPY_PHASE_STRIP = NO; 144 | GCC_DYNAMIC_NO_PIC = NO; 145 | GCC_ENABLE_FIX_AND_CONTINUE = YES; 146 | GCC_MODEL_TUNING = G5; 147 | GCC_OPTIMIZATION_LEVEL = 0; 148 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 149 | INFOPLIST_FILE = "$(SRCROOT)/message-indicator/Info.plist"; 150 | INSTALL_PATH = "$(HOME)/Library/Bundles"; 151 | PRODUCT_BUNDLE_IDENTIFIER = "com.hackingdartmouth.${PRODUCT_NAME:rfc1034Identifier}"; 152 | PRODUCT_NAME = "message-indicator"; 153 | WRAPPER_EXTENSION = bundle; 154 | }; 155 | name = Debug; 156 | }; 157 | 1DEB913C08733D840010E9CD /* Release */ = { 158 | isa = XCBuildConfiguration; 159 | buildSettings = { 160 | ALWAYS_SEARCH_USER_PATHS = NO; 161 | COMBINE_HIDPI_IMAGES = YES; 162 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 163 | GCC_MODEL_TUNING = G5; 164 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 165 | INFOPLIST_FILE = "$(SRCROOT)/message-indicator/Info.plist"; 166 | INSTALL_PATH = "$(HOME)/Library/Bundles"; 167 | PRODUCT_BUNDLE_IDENTIFIER = "com.hackingdartmouth.${PRODUCT_NAME:rfc1034Identifier}"; 168 | PRODUCT_NAME = "message-indicator"; 169 | WRAPPER_EXTENSION = bundle; 170 | }; 171 | name = Release; 172 | }; 173 | 1DEB913F08733D840010E9CD /* Debug */ = { 174 | isa = XCBuildConfiguration; 175 | buildSettings = { 176 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 177 | CLANG_ENABLE_MODULES = YES; 178 | CLANG_WARN_BOOL_CONVERSION = YES; 179 | CLANG_WARN_CONSTANT_CONVERSION = YES; 180 | CLANG_WARN_EMPTY_BODY = YES; 181 | CLANG_WARN_ENUM_CONVERSION = YES; 182 | CLANG_WARN_INFINITE_RECURSION = YES; 183 | CLANG_WARN_INT_CONVERSION = YES; 184 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 185 | CLANG_WARN_UNREACHABLE_CODE = YES; 186 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 187 | ENABLE_STRICT_OBJC_MSGSEND = YES; 188 | ENABLE_TESTABILITY = YES; 189 | GCC_C_LANGUAGE_STANDARD = gnu99; 190 | GCC_NO_COMMON_BLOCKS = YES; 191 | GCC_OPTIMIZATION_LEVEL = 0; 192 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 193 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 194 | GCC_WARN_UNDECLARED_SELECTOR = YES; 195 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 196 | GCC_WARN_UNUSED_FUNCTION = YES; 197 | GCC_WARN_UNUSED_VARIABLE = YES; 198 | MACOSX_DEPLOYMENT_TARGET = 10.9; 199 | ONLY_ACTIVE_ARCH = YES; 200 | PREBINDING = NO; 201 | SDKROOT = macosx; 202 | }; 203 | name = Debug; 204 | }; 205 | 1DEB914008733D840010E9CD /* Release */ = { 206 | isa = XCBuildConfiguration; 207 | buildSettings = { 208 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_WARN_BOOL_CONVERSION = YES; 211 | CLANG_WARN_CONSTANT_CONVERSION = YES; 212 | CLANG_WARN_EMPTY_BODY = YES; 213 | CLANG_WARN_ENUM_CONVERSION = YES; 214 | CLANG_WARN_INFINITE_RECURSION = YES; 215 | CLANG_WARN_INT_CONVERSION = YES; 216 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 217 | CLANG_WARN_UNREACHABLE_CODE = YES; 218 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 219 | ENABLE_STRICT_OBJC_MSGSEND = YES; 220 | GCC_C_LANGUAGE_STANDARD = gnu99; 221 | GCC_NO_COMMON_BLOCKS = YES; 222 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 223 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 224 | GCC_WARN_UNDECLARED_SELECTOR = YES; 225 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 226 | GCC_WARN_UNUSED_FUNCTION = YES; 227 | GCC_WARN_UNUSED_VARIABLE = YES; 228 | MACOSX_DEPLOYMENT_TARGET = 10.9; 229 | ONLY_ACTIVE_ARCH = NO; 230 | PREBINDING = NO; 231 | SDKROOT = macosx; 232 | }; 233 | name = Release; 234 | }; 235 | /* End XCBuildConfiguration section */ 236 | 237 | /* Begin XCConfigurationList section */ 238 | 1DEB913A08733D840010E9CD /* Build configuration list for PBXNativeTarget "message-indicator" */ = { 239 | isa = XCConfigurationList; 240 | buildConfigurations = ( 241 | 1DEB913B08733D840010E9CD /* Debug */, 242 | 1DEB913C08733D840010E9CD /* Release */, 243 | ); 244 | defaultConfigurationIsVisible = 0; 245 | defaultConfigurationName = Release; 246 | }; 247 | 1DEB913E08733D840010E9CD /* Build configuration list for PBXProject "message-indicator" */ = { 248 | isa = XCConfigurationList; 249 | buildConfigurations = ( 250 | 1DEB913F08733D840010E9CD /* Debug */, 251 | 1DEB914008733D840010E9CD /* Release */, 252 | ); 253 | defaultConfigurationIsVisible = 0; 254 | defaultConfigurationName = Release; 255 | }; 256 | /* End XCConfigurationList section */ 257 | }; 258 | rootObject = 089C1669FE841209C02AAC07 /* Project object */; 259 | } 260 | -------------------------------------------------------------------------------- /message-indicator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /message-indicator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /message-indicator.xcodeproj/project.xcworkspace/xcuserdata/abeals.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dado3212/message-indicator/c4961e3e72a558f5ec1eb8385f6678e93b1ecb8e/message-indicator.xcodeproj/project.xcworkspace/xcuserdata/abeals.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /message-indicator.xcodeproj/project.xcworkspace/xcuserdata/alexbeals.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dado3212/message-indicator/c4961e3e72a558f5ec1eb8385f6678e93b1ecb8e/message-indicator.xcodeproj/project.xcworkspace/xcuserdata/alexbeals.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /message-indicator.xcodeproj/project.xcworkspace/xcuserdata/alexbeals.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /message-indicator.xcodeproj/xcuserdata/abeals.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | message-indicator.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /message-indicator.xcodeproj/xcuserdata/alexbeals.xcuserdatad/xcschemes/message-indicator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /message-indicator.xcodeproj/xcuserdata/alexbeals.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | message-indicator.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 8D5B49AC048680CD000E48DA 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /message-indicator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.2.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSHumanReadableCopyright 26 | Copyright © 2017, Alex Beals 27 | NSPrincipalClass 28 | 29 | SIMBLTargetApplications 30 | 31 | 32 | 33 | BundleIdentifier 34 | com.apple.iChat 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /message-indicator/ZKSwizzle.h: -------------------------------------------------------------------------------- 1 | // 2 | // ZKSwizzle.h 3 | // ZKSwizzle 4 | // 5 | // Created by Alexander S Zielenski on 7/24/14. 6 | // Copyright (c) 2014 Alexander S Zielenski. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | // This is a class for streamlining swizzling. Simply create a new class of any name you want and 14 | // Example: 15 | /* 16 | @interface ZKHookClass : NSObject 17 | - (NSString *)description; // hooks -description on NSObject 18 | - (void)addedMethod; // all subclasses of NSObject now respond to -addedMethod 19 | @end 20 | 21 | @implementation ZKHookClass 22 | ... 23 | @end 24 | 25 | [ZKSwizzle swizzleClass:ZKClass(ZKHookClass) forClass:ZKClass(destination)]; 26 | */ 27 | 28 | #ifndef ZKSWIZZLE_DEFS 29 | #define ZKSWIZZLE_DEFS 30 | 31 | // CRAZY MACROS FOR DYNAMIC PROTOTYPE CREATION 32 | #define VA_NUM_ARGS(...) VA_NUM_ARGS_IMPL(0, ## __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5 ,4 ,3 ,2, 1, 0) 33 | #define VA_NUM_ARGS_IMPL(_0, _1,_2,_3,_4,_5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20 ,N,...) N 34 | 35 | #define WRAP0() 36 | #define WRAP1(VARIABLE) , typeof ( VARIABLE ) 37 | #define WRAP2(VARIABLE, ...) WRAP1(VARIABLE) WRAP1(__VA_ARGS__) 38 | #define WRAP3(VARIABLE, ...) WRAP1(VARIABLE) WRAP2(__VA_ARGS__) 39 | #define WRAP4(VARIABLE, ...) WRAP1(VARIABLE) WRAP3(__VA_ARGS__) 40 | #define WRAP5(VARIABLE, ...) WRAP1(VARIABLE) WRAP4(__VA_ARGS__) 41 | #define WRAP6(VARIABLE, ...) WRAP1(VARIABLE) WRAP5(__VA_ARGS__) 42 | #define WRAP7(VARIABLE, ...) WRAP1(VARIABLE) WRAP6(__VA_ARGS__) 43 | #define WRAP8(VARIABLE, ...) WRAP1(VARIABLE) WRAP7(__VA_ARGS__) 44 | #define WRAP9(VARIABLE, ...) WRAP1(VARIABLE) WRAP8(__VA_ARGS__) 45 | #define WRAP10(VARIABLE, ...) WRAP1(VARIABLE) WRAP9(__VA_ARGS__) 46 | #define WRAP11(VARIABLE, ...) WRAP1(VARIABLE) WRAP10(__VA_ARGS__) 47 | #define WRAP12(VARIABLE, ...) WRAP1(VARIABLE) WRAP11(__VA_ARGS__) 48 | #define WRAP13(VARIABLE, ...) WRAP1(VARIABLE) WRAP12(__VA_ARGS__) 49 | #define WRAP14(VARIABLE, ...) WRAP1(VARIABLE) WRAP13(__VA_ARGS__) 50 | #define WRAP15(VARIABLE, ...) WRAP1(VARIABLE) WRAP14(__VA_ARGS__) 51 | #define WRAP16(VARIABLE, ...) WRAP1(VARIABLE) WRAP15(__VA_ARGS__) 52 | #define WRAP17(VARIABLE, ...) WRAP1(VARIABLE) WRAP16(__VA_ARGS__) 53 | #define WRAP18(VARIABLE, ...) WRAP1(VARIABLE) WRAP17(__VA_ARGS__) 54 | #define WRAP19(VARIABLE, ...) WRAP1(VARIABLE) WRAP18(__VA_ARGS__) 55 | #define WRAP20(VARIABLE, ...) WRAP1(VARIABLE) WRAP19(__VA_ARGS__) 56 | 57 | #define CAT(A, B) A ## B 58 | #define INVOKE(MACRO, NUMBER, ...) CAT(MACRO, NUMBER)(__VA_ARGS__) 59 | #define WRAP_LIST(...) INVOKE(WRAP, VA_NUM_ARGS(__VA_ARGS__), __VA_ARGS__) 60 | 61 | // Gets the a class with the name CLASS 62 | #define ZKClass(CLASS) objc_getClass(#CLASS) 63 | 64 | // returns the value of an instance variable. 65 | #if !__has_feature(objc_arc) 66 | #define ZKHookIvar(OBJECT, TYPE, NAME) (*(TYPE *)ZKIvarPointer(OBJECT, NAME)) 67 | #else 68 | #define ZKHookIvar(OBJECT, TYPE, NAME) \ 69 | _Pragma("clang diagnostic push") \ 70 | _Pragma("clang diagnostic ignored \"-Wignored-attributes\"") \ 71 | (*(__unsafe_unretained TYPE *)ZKIvarPointer(OBJECT, NAME)) \ 72 | _Pragma("clang diagnostic pop") 73 | #endif 74 | 75 | //////////////////////////////////////////////////////////////////////////////// 76 | //// Core Macros (For fine-tuned Use) 77 | //////////////////////////////////////////////////////////////////////////////// 78 | // returns the original implementation of the swizzled function or null or not found 79 | #define ZKOrig(TYPE, ...) ((TYPE (*)(id, SEL WRAP_LIST(__VA_ARGS__)))(ZKOriginalImplementation(self, _cmd, __PRETTY_FUNCTION__)))(self, _cmd, ##__VA_ARGS__) 80 | 81 | // returns the original implementation of the superclass of the object swizzled 82 | #define ZKSuper(TYPE, ...) ((TYPE (*)(id, SEL WRAP_LIST(__VA_ARGS__)))(ZKSuperImplementation(self, _cmd, __PRETTY_FUNCTION__)))(self, _cmd, ##__VA_ARGS__) 83 | 84 | #define _ZKSwizzleInterfaceConditionally(CLASS_NAME, TARGET_CLASS, SUPERCLASS, GROUP, IMMEDIATELY) \ 85 | @interface _$ ## CLASS_NAME : SUPERCLASS @end\ 86 | @implementation _$ ## CLASS_NAME\ 87 | + (void)initialize {}\ 88 | @end\ 89 | @interface CLASS_NAME : _$ ## CLASS_NAME @end\ 90 | @implementation CLASS_NAME (ZKSWIZZLE)\ 91 | + (void)load {\ 92 | if (IMMEDIATELY) {\ 93 | [self _ZK_unconditionallySwizzle];\ 94 | } else {\ 95 | _$ZKRegisterInterface(self, #GROUP);\ 96 | }\ 97 | }\ 98 | + (void)_ZK_unconditionallySwizzle {\ 99 | ZKSwizzle(CLASS_NAME, TARGET_CLASS);\ 100 | }\ 101 | @end 102 | 103 | // Bootstraps your swizzling class so that it requires no setup 104 | // outside of this macro call 105 | // If you override +load you must call ZKSwizzle(CLASS_NAME, TARGET_CLASS) 106 | // yourself, otherwise the swizzling would not take place 107 | #define ZKSwizzleInterface(CLASS_NAME, TARGET_CLASS, SUPERCLASS) \ 108 | _ZKSwizzleInterfaceConditionally(CLASS_NAME, TARGET_CLASS, SUPERCLASS, ZK_UNGROUPED, YES) 109 | 110 | // Same as ZKSwizzleInterface, except 111 | #define ZKSwizzleInterfaceGroup(CLASS_NAME, TARGET_CLASS, SUPER_CLASS, GROUP) \ 112 | _ZKSwizzleInterfaceConditionally(CLASS_NAME, TARGET_CLASS, SUPER_CLASS, GROUP, NO) 113 | 114 | //////////////////////////////////////////////////////////////////////////////// 115 | //// Sugar Macros (For general use) 116 | //////////////////////////////////////////////////////////////////////////////// 117 | // Inspired by logos. Credits to @mstg! 118 | 119 | #define __GEN_CLASS(TARGET, LINE) __ZK_## LINE## TARGET 120 | #define _GEN_CLASS(TARGET, LINE) __GEN_CLASS(TARGET, LINE) 121 | #define GEN_CLASS(TARGET) _GEN_CLASS(TARGET, __LINE__) 122 | 123 | #define hook_2(TARGET, GROUP) \ 124 | ZKSwizzleInterfaceGroup(GEN_CLASS(TARGET), TARGET, NSObject, GROUP) @implementation GEN_CLASS(TARGET) 125 | 126 | #define hook_1(TARGET) \ 127 | ZKSwizzleInterface(GEN_CLASS(TARGET), TARGET, NSObject) @implementation GEN_CLASS(TARGET) 128 | 129 | #define endhook @end 130 | 131 | #define _orig(...) ZKOrig(__VA_ARGS__) 132 | #define _super(...) ZKSuper(__VA_ARGS__) 133 | 134 | #define __HOOK(ARGC, ARGS...) hook_ ## ARGC (ARGS) 135 | #define _HOOK(ARGC, ARGS...) __HOOK(ARGC, ARGS) 136 | #define hook(...) _HOOK(VA_NUM_ARGS(__VA_ARGS__), __VA_ARGS__) 137 | #define ctor __attribute__((constructor)) void init() 138 | 139 | #define ZKIgnoreTypes +(BOOL)_ZK_ignoreTypes { return YES; } 140 | 141 | __BEGIN_DECLS 142 | 143 | //////////////////////////////////////////////////////////////////////////////// 144 | //// C Backing (Don't typically call directly) 145 | //////////////////////////////////////////////////////////////////////////////// 146 | 147 | // Make sure to cast this before you use it 148 | typedef id (*ZKIMP)(id, SEL, ...); 149 | 150 | // returns a pointer to the instance variable "name" on the object 151 | void *ZKIvarPointer(id self, const char *name); 152 | // returns the original implementation of a method with selector "sel" of an object hooked by the methods below 153 | ZKIMP ZKOriginalImplementation(id self, SEL sel, const char *info); 154 | // returns the implementation of a method with selector "sel" of the superclass of object 155 | ZKIMP ZKSuperImplementation(id object, SEL sel, const char *info); 156 | 157 | // hooks all the implemented methods of source with destination 158 | // adds any methods that arent implemented on destination to destination that are implemented in source 159 | #define ZKSwizzle(src, dst) _ZKSwizzle(ZKClass(src), ZKClass(dst)) 160 | BOOL _ZKSwizzle(Class src, Class dest); 161 | 162 | #define ZKSwizzleGroup(NAME) _ZKSwizzleGroup(#NAME) 163 | void _$ZKRegisterInterface(Class cls, const char *groupName); 164 | BOOL _ZKSwizzleGroup(const char *groupName); 165 | 166 | // Calls above method with the superclass of source for desination 167 | #define ZKSwizzleClass(src) _ZKSwizzleClass(ZKClass(src)) 168 | BOOL _ZKSwizzleClass(Class cls); 169 | 170 | __END_DECLS 171 | #endif 172 | 173 | -------------------------------------------------------------------------------- /message-indicator/ZKSwizzle.m: -------------------------------------------------------------------------------- 1 | // 2 | // ZKSwizzle.m 3 | // ZKSwizzle 4 | // 5 | // Created by Alexander S Zielenski on 7/24/14. 6 | // Copyright (c) 2014 Alexander S Zielenski. All rights reserved. 7 | // 8 | 9 | #import "ZKSwizzle.h" 10 | static NSMutableDictionary *classTable; 11 | 12 | @interface NSObject (ZKSwizzle) 13 | + (void)_ZK_unconditionallySwizzle; 14 | + (BOOL)_ZK_ignoreTypes; 15 | @end 16 | 17 | void *ZKIvarPointer(id self, const char *name) { 18 | Ivar ivar = class_getInstanceVariable(object_getClass(self), name); 19 | return ivar == NULL ? NULL : (__bridge void *)self + ivar_getOffset(ivar); 20 | } 21 | 22 | static SEL destinationSelectorForSelector(SEL cmd, Class dst) { 23 | return NSSelectorFromString([@"_ZK_old_" stringByAppendingFormat:@"%s_%@", class_getName(dst), NSStringFromSelector(cmd)]); 24 | } 25 | 26 | static Class classFromInfo(const char *info) { 27 | NSUInteger bracket_index = -1; 28 | for (NSUInteger i = 0; i < strlen(info); i++) { 29 | if (info[i] == '[') { 30 | bracket_index = i; 31 | break; 32 | } 33 | } 34 | bracket_index++; 35 | 36 | if (bracket_index == -1) { 37 | [NSException raise:@"Failed to parse info" format:@"Couldn't find swizzle class for info: %s", info]; 38 | return NULL; 39 | } 40 | 41 | char after_bracket[255]; 42 | memcpy(after_bracket, &info[bracket_index], strlen(info) - bracket_index - 1); 43 | 44 | for (NSUInteger i = 0; i < strlen(info); i++) { 45 | if (after_bracket[i] == ' ') { 46 | after_bracket[i] = '\0'; 47 | } 48 | } 49 | 50 | return objc_getClass(after_bracket); 51 | } 52 | 53 | // takes __PRETTY_FUNCTION__ for info which gives the name of the swizzle source class 54 | /* 55 | 56 | We add the original implementation onto the swizzle class 57 | On ZKOrig, we use __PRETTY_FUNCTION__ to get the name of the swizzle class 58 | Then we get the implementation of that selector on the swizzle class 59 | Then we call it directly, passing in the correct selector and self 60 | 61 | */ 62 | ZKIMP ZKOriginalImplementation(id self, SEL sel, const char *info) { 63 | if (sel == NULL || self == NULL || info == NULL) { 64 | [NSException raise:@"Invalid Arguments" format:@"One of self: %@, self: %@, or info: %s is NULL", self, NSStringFromSelector(sel), info]; 65 | return NULL; 66 | } 67 | 68 | Class cls = classFromInfo(info); 69 | Class dest = object_getClass(self); 70 | 71 | if (cls == NULL || dest == NULL) { 72 | [NSException raise:@"Failed obtain class pair" format:@"src: %@ | dst: %@ | sel: %@", NSStringFromClass(cls), NSStringFromClass(dest), NSStringFromSelector(sel)]; 73 | return NULL; 74 | } 75 | 76 | SEL destSel = destinationSelectorForSelector(sel, cls); 77 | 78 | Method method = class_getInstanceMethod(dest, destSel); 79 | 80 | if (method == NULL) { 81 | [NSException raise:@"Failed to retrieve method" format:@"Got null for the source class %@ with selector %@ (%@)", NSStringFromClass(cls), NSStringFromSelector(sel), NSStringFromSelector(destSel)]; 82 | return NULL; 83 | } 84 | 85 | ZKIMP implementation = (ZKIMP)method_getImplementation(method); 86 | if (implementation == NULL) { 87 | [NSException raise:@"Failed to get implementation" format:@"The objective-c runtime could not get the implementation for %@ on the class %@. There is no fix for this", NSStringFromClass(cls), NSStringFromSelector(sel)]; 88 | } 89 | 90 | return implementation; 91 | } 92 | 93 | ZKIMP ZKSuperImplementation(id object, SEL sel, const char *info) { 94 | if (sel == NULL || object == NULL) { 95 | [NSException raise:@"Invalid Arguments" format:@"One of self: %@, self: %@ is NULL", object, NSStringFromSelector(sel)]; 96 | return NULL; 97 | } 98 | 99 | Class cls = object_getClass(object); 100 | if (cls == NULL) { 101 | [NSException raise:@"Invalid Argument" format:@"Could not obtain class for the passed object"]; 102 | return NULL; 103 | } 104 | 105 | // Two scenarios: 106 | // 1.) The superclass was not swizzled, no problem 107 | // 2.) The superclass was swizzled, problem 108 | 109 | // We want to return the swizzled class's superclass implementation 110 | // If this is a subclass of such a class, we want two behaviors: 111 | // a.) If this imp was also swizzled, no problem, return the superclass's swizzled imp 112 | // b.) This imp was not swizzled, return the class that was originally swizzled's superclass's imp 113 | Class sourceClass = classFromInfo(info); 114 | if (sourceClass != NULL) { 115 | BOOL isClassMethod = class_isMetaClass(cls); 116 | // This was called from a swizzled method, get the class it was swizzled with 117 | NSString *className = classTable[NSStringFromClass(sourceClass)]; 118 | if (className != NULL) { 119 | cls = NSClassFromString(className); 120 | // make sure we get a class method if we asked for one 121 | if (isClassMethod) { 122 | cls = object_getClass(cls); 123 | } 124 | } 125 | } 126 | 127 | cls = class_getSuperclass(cls); 128 | 129 | // This is a root class, it has no super class 130 | if (cls == NULL) { 131 | [NSException raise:@"Invalid Argument" format:@"Could not obtain superclass for the passed object"]; 132 | return NULL; 133 | } 134 | 135 | Method method = class_getInstanceMethod(cls, sel); 136 | if (method == NULL) { 137 | [NSException raise:@"Failed to retrieve method" format:@"We could not find the super implementation for the class %@ and selector %@, are you sure it exists?", NSStringFromClass(cls), NSStringFromSelector(sel)]; 138 | return NULL; 139 | } 140 | 141 | ZKIMP implementation = (ZKIMP)method_getImplementation(method); 142 | if (implementation == NULL) { 143 | [NSException raise:@"Failed to get implementation" format:@"The objective-c runtime could not get the implementation for %@ on the class %@. There is no fix for this", NSStringFromClass(cls), NSStringFromSelector(sel)]; 144 | } 145 | 146 | return implementation; 147 | } 148 | 149 | static BOOL enumerateMethods(Class, Class); 150 | BOOL _ZKSwizzle(Class src, Class dest) { 151 | if (dest == NULL) 152 | return NO; 153 | 154 | NSString *destName = NSStringFromClass(dest); 155 | if (!destName) { 156 | return NO; 157 | } 158 | 159 | if (!classTable) { 160 | classTable = [[NSMutableDictionary alloc] init]; 161 | } 162 | 163 | if ([classTable objectForKey:NSStringFromClass(src)]) { 164 | [NSException raise:@"Invalid Argument" 165 | format:@"This source class (%@) was already swizzled with another, (%@)", NSStringFromClass(src), classTable[NSStringFromClass(src)]]; 166 | return NO; 167 | } 168 | 169 | BOOL success = enumerateMethods(dest, src); 170 | // The above method only gets instance methods. Do the same method for the metaclass of the class 171 | success &= enumerateMethods(object_getClass(dest), object_getClass(src)); 172 | 173 | [classTable setObject:destName forKey:NSStringFromClass(src)]; 174 | return success; 175 | } 176 | 177 | BOOL _ZKSwizzleClass(Class cls) { 178 | return _ZKSwizzle(cls, [cls superclass]); 179 | } 180 | 181 | static BOOL classIgnoresTypes(Class cls) { 182 | if (!class_isMetaClass(cls)) { 183 | cls = object_getClass(cls); 184 | } 185 | 186 | if (class_respondsToSelector(cls, @selector(_ZK_ignoreTypes))) { 187 | Class cls2 = class_createInstance(cls, 0); 188 | return [cls2 _ZK_ignoreTypes]; 189 | } 190 | 191 | return NO; 192 | } 193 | 194 | static BOOL enumerateMethods(Class destination, Class source) { 195 | #if OBJC_API_VERSION < 2 196 | [NSException raise:@"Unsupported feature" format:@"ZKSwizzle is only available in objc 2.0"]; 197 | return NO; 198 | 199 | #else 200 | 201 | unsigned int methodCount; 202 | Method *methodList = class_copyMethodList(source, &methodCount); 203 | BOOL success = YES; 204 | BOOL ignoreTypes = classIgnoresTypes(source); 205 | 206 | for (int i = 0; i < methodCount; i++) { 207 | Method method = methodList[i]; 208 | SEL selector = method_getName(method); 209 | NSString *methodName = NSStringFromSelector(selector); 210 | 211 | // Don't do anything with the unconditional swizzle 212 | if (sel_isEqual(selector, @selector(_ZK_unconditionallySwizzle)) || 213 | sel_isEqual(selector, @selector(_ZK_ignoreTypes))) { 214 | continue; 215 | } 216 | 217 | // We only swizzle methods that are implemented 218 | if (class_respondsToSelector(destination, selector)) { 219 | Method originalMethod = class_getInstanceMethod(destination, selector); 220 | 221 | const char *originalType = method_getTypeEncoding(originalMethod); 222 | const char *newType = method_getTypeEncoding(method); 223 | if (strcmp(originalType, newType) != 0 && !ignoreTypes) { 224 | NSLog(@"ZKSwizzle: incompatible type encoding for %@. (expected %s, got %s)", methodName, originalType, newType); 225 | // Incompatible type encoding 226 | success = NO; 227 | continue; 228 | } 229 | 230 | // We are re-adding the destination selector because it could be on a superclass and not on the class itself. This method could fail 231 | class_addMethod(destination, selector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 232 | 233 | SEL destSel = destinationSelectorForSelector(selector, source); 234 | if (!class_addMethod(destination, destSel, method_getImplementation(method), method_getTypeEncoding(originalMethod))) { 235 | NSLog(@"ZKSwizzle: failed to add method %@ onto class %@ with selector %@", NSStringFromSelector(selector), NSStringFromClass(source), NSStringFromSelector(destSel)); 236 | success = NO; 237 | continue; 238 | } 239 | 240 | method_exchangeImplementations(class_getInstanceMethod(destination, selector), class_getInstanceMethod(destination, destSel)); 241 | } else { 242 | // Add any extra methods to the class but don't swizzle them 243 | success &= class_addMethod(destination, selector, method_getImplementation(method), method_getTypeEncoding(method)); 244 | } 245 | } 246 | 247 | unsigned int propertyCount; 248 | objc_property_t *propertyList = class_copyPropertyList(source, &propertyCount); 249 | for (int i = 0; i < propertyCount; i++) { 250 | objc_property_t property = propertyList[i]; 251 | const char *name = property_getName(property); 252 | unsigned int attributeCount; 253 | objc_property_attribute_t *attributes = property_copyAttributeList(property, &attributeCount); 254 | 255 | if (class_getProperty(destination, name) == NULL) { 256 | class_addProperty(destination, name, attributes, attributeCount); 257 | } else { 258 | class_replaceProperty(destination, name, attributes, attributeCount); 259 | } 260 | 261 | free(attributes); 262 | } 263 | 264 | free(propertyList); 265 | free(methodList); 266 | return success; 267 | #endif 268 | } 269 | 270 | // Options were to use a group class and traverse its subclasses 271 | // or to create a groups dictionary 272 | // This works because +load on NSObject is called before attribute((constructor)) 273 | static NSMutableDictionary *groups = nil; 274 | void _$ZKRegisterInterface(Class cls, const char *groupName) { 275 | if (!groups) 276 | groups = [[NSMutableDictionary dictionary] retain]; 277 | 278 | NSString *groupString = @(groupName); 279 | NSMutableArray *groupList = groups[groupString]; 280 | if (!groupList) { 281 | groupList = [NSMutableArray array]; 282 | groups[groupString] = groupList; 283 | } 284 | 285 | [groupList addObject:NSStringFromClass(cls)]; 286 | } 287 | 288 | BOOL _ZKSwizzleGroup(const char *groupName) { 289 | NSArray *groupList = groups[@(groupName)]; 290 | if (!groupList) { 291 | [NSException raise:@"Invalid Argument" format:@"ZKSwizzle: There is no group by the name of %s", groupName]; 292 | return NO; 293 | } 294 | 295 | BOOL success = YES; 296 | for (NSString *className in groupList) { 297 | Class cls = NSClassFromString(className); 298 | if (cls == NULL) 299 | continue; 300 | 301 | if (class_respondsToSelector(object_getClass(cls), @selector(_ZK_unconditionallySwizzle))) { 302 | [cls _ZK_unconditionallySwizzle]; 303 | } else { 304 | success = NO; 305 | } 306 | } 307 | 308 | return success; 309 | } 310 | -------------------------------------------------------------------------------- /message-indicator/messageIndicator.m: -------------------------------------------------------------------------------- 1 | // 2 | // message-indicator.m 3 | // message-indicator 4 | // 5 | // Created by Alex Beals 6 | // Copyright 2017 Alex Beals. 7 | // 8 | 9 | @import AppKit; 10 | #import "ZKSwizzle.h" 11 | #import 12 | 13 | @interface IMMessage : NSObject 14 | @property(retain, nonatomic) NSString *guid; 15 | @property(readonly, nonatomic) BOOL isFromMe; 16 | @end 17 | 18 | @interface IMChat : NSObject 19 | @property(readonly, nonatomic) NSString *guid; 20 | @property(readonly, nonatomic) IMMessage *lastFinishedMessage; 21 | @property(readonly, nonatomic) NSArray *participants; 22 | @end 23 | 24 | @interface SOChatDisplayController : NSObject 25 | @property(retain, nonatomic) IMChat *chat; 26 | @end 27 | 28 | @interface ChatTableCellView: NSObject 29 | @property(readonly) SOChatDisplayController *chatDisplayController; 30 | @property(retain) NSView *unreadIndicator; 31 | @end 32 | 33 | @interface MessageIndicator: NSView 34 | @property(retain, nonatomic) IMChat *chat; 35 | @end 36 | 37 | @implementation MessageIndicator 38 | - (id)initWithChat:(IMChat *)chat { 39 | NSRect frame = CGRectMake(6, 12, 9, 9); 40 | self = [super initWithFrame:frame]; 41 | if (self) { 42 | self.wantsLayer = true; 43 | self.layer.backgroundColor = [NSColor colorWithCalibratedRed:85.0f/255.0f green:85.0f/255.0f blue:90.0f/255.0f alpha:1.0f].CGColor; 44 | self.layer.cornerRadius = frame.size.width * 0.5; 45 | self.layer.masksToBounds = true; 46 | 47 | self.chat = chat; 48 | } 49 | return self; 50 | } 51 | 52 | - (void)mouseDown:(NSEvent *)event { 53 | // Control and click 54 | if (event.modifierFlags & NSControlKeyMask) { 55 | // Saves the ID for the most recent message in the chat, to hide it 56 | NSString *messageGuid = self.chat.lastFinishedMessage.guid; 57 | NSString *chatGuid = self.chat.guid; 58 | 59 | NSUserDefaults *cachedDefaults = [NSUserDefaults standardUserDefaults]; 60 | NSMutableDictionary *cached = [[cachedDefaults dictionaryForKey:@"cached"] mutableCopy]; 61 | 62 | if (cached == nil) { 63 | cached = [[NSMutableDictionary alloc] init]; 64 | } 65 | cached[chatGuid] = messageGuid; 66 | [cachedDefaults setObject:cached forKey:@"cached"]; 67 | [cachedDefaults synchronize]; 68 | 69 | [self setHidden:true]; 70 | } 71 | } 72 | @end 73 | 74 | // Gets the indicator layer if it exists by recursing on subviews 75 | static MessageIndicator* getIndicator(NSView *view) { 76 | for (NSView *subview in [view subviews]) { 77 | if ([subview isKindOfClass: [MessageIndicator class]]) { 78 | return (MessageIndicator *)subview; 79 | } 80 | } 81 | 82 | return nil; 83 | } 84 | 85 | // Hooks into the ChatTableCellView and hijacks the layouts to add the indicator 86 | ZKSwizzleInterface(custom_cellView, ChatTableCellView, NSTableCellView) 87 | @implementation custom_cellView 88 | 89 | // Ran when the summary for a cell changes (new message either direction) 90 | - (void)_updateSummary { 91 | ZKOrig(void); 92 | 93 | // Indicator if: 94 | // - you weren't the last person to send a message 95 | // - it's not a group message 96 | bool needsResponse = !((ChatTableCellView *)self).chatDisplayController.chat.lastFinishedMessage.isFromMe && ((ChatTableCellView *)self).chatDisplayController.chat.participants.count == 1; 97 | 98 | // Check from cache, limit only if could change needsResponse 99 | if (needsResponse) { 100 | NSString *messageGuid = ((ChatTableCellView *)self).chatDisplayController.chat.lastFinishedMessage.guid; 101 | NSString *chatGuid =((ChatTableCellView *)self).chatDisplayController.chat.guid; 102 | 103 | NSUserDefaults *cachedDefaults = [NSUserDefaults standardUserDefaults]; 104 | NSMutableDictionary *cached = [[cachedDefaults dictionaryForKey:@"cached"] mutableCopy]; 105 | 106 | // If it needs response but the most recent is saved in the cleared caching, then don't show indicator 107 | if (cached && cached[chatGuid]) { 108 | if ([cached[chatGuid] isEqualToString:messageGuid]) { 109 | needsResponse = false; 110 | } else { 111 | // If it's been updated, save space by removing the chat from the array 112 | [cached removeObjectForKey:chatGuid]; 113 | [cachedDefaults setObject:cached forKey:@"cached"]; 114 | [cachedDefaults synchronize]; 115 | } 116 | } 117 | [cached release]; 118 | } 119 | 120 | // Get current indicator (check to see if it's already there) 121 | MessageIndicator *currentIndicator = getIndicator(self); 122 | 123 | // If no indicator, then create and add it 124 | if (currentIndicator == nil) { 125 | // Makes the "unresponded to" indicator 126 | MessageIndicator *newIndicator = [[MessageIndicator alloc] initWithChat:((ChatTableCellView *)self).chatDisplayController.chat]; 127 | 128 | [self addSubview:newIndicator]; 129 | 130 | currentIndicator = newIndicator; 131 | } 132 | 133 | // Toggle visibility based on status 134 | if (needsResponse) { 135 | currentIndicator.chat = ((ChatTableCellView *) self).chatDisplayController.chat; 136 | [currentIndicator setHidden:false]; 137 | } else if (!needsResponse) { 138 | [currentIndicator setHidden:true]; 139 | } 140 | } 141 | @end 142 | -------------------------------------------------------------------------------- /preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dado3212/message-indicator/c4961e3e72a558f5ec1eb8385f6678e93b1ecb8e/preview.jpg --------------------------------------------------------------------------------