├── .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
--------------------------------------------------------------------------------