diff --git a/ChatSDKCore/Assets/.gitkeep b/ChatSDKCore/Assets/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/ChatSDKCore/Classes/.gitkeep b/ChatSDKCore/Classes/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/ChatSDKCore/Classes/Categories/NSDate+Additions.m b/ChatSDKCore/Classes/Categories/NSDate+Additions.m index f9fe23fa..f3794376 100755 --- a/ChatSDKCore/Classes/Categories/NSDate+Additions.m +++ b/ChatSDKCore/Classes/Categories/NSDate+Additions.m @@ -24,7 +24,7 @@ -(NSString *) threadTimeAgo { // We check if the last date was in the last few days // Then check if it was exactly yesterday if ([self daysAgo] < 3 && today.day == otherDay.day + 1) { - time = [NSBundle t: bYesterday]; + time = [NSBundle t: NSLocalizedString(bYesterday, nil)]; } else if (self.daysAgo > 1 && self.daysAgo < 7) { [formatter setDateFormat:@"EEEE"]; @@ -62,10 +62,10 @@ -(NSString *) dateAgo { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; if (self.isToday) { - day = [NSBundle t: bToday]; + day = [NSBundle t: NSLocalizedString(bToday, nil)]; } else if (self.isYesterday) { - day = [NSBundle t: bYesterday]; + day = [NSBundle t: NSLocalizedString(bYesterday, nil)]; } else if (self.daysAgo < 7) { [formatter setDateFormat:@"EEE"]; diff --git a/ChatSDKCore/Classes/Entities/PMessage.h b/ChatSDKCore/Classes/Entities/PMessage.h index ca604282..63f9c0a4 100755 --- a/ChatSDKCore/Classes/Entities/PMessage.h +++ b/ChatSDKCore/Classes/Entities/PMessage.h @@ -21,6 +21,10 @@ typedef enum { bMessageTypeSticker = 6, bMessageTypeFile = 7, bMessageTypeCustom = 99, + bMessageTypeLeaveConversation = 8, + bMessageTypeAddedMember = 9, + bMessageTypeGroupRenamed = 10, + } bMessageType; typedef enum { diff --git a/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.h b/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.h index 6ebfeb8f..6de9348d 100755 --- a/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.h +++ b/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.h @@ -19,5 +19,5 @@ -(id) createThreadWithUsers: (NSArray *) users name: (NSString *) name type: (bThreadType) type; -(RXPromise *) prepareSendMessage: (id) messageModel; - +-(RXPromise *) updateThread: (id) threadModel dataPushed: (void(^)(NSError * error, id thread)) dataPushed; @end diff --git a/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.m b/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.m index 235cf906..4df38f52 100755 --- a/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.m +++ b/ChatSDKCore/Classes/Handlers/Abstract/BAbstractCoreHandler.m @@ -216,11 +216,17 @@ -(RXPromise *) createThreadWithUsers: (NSArray *) users threadCreated:threadCreated]; } +-(RXPromise *) updateThread: (id) threadModel dataPushed: (void(^)(NSError * error, id thread)) dataPushed { + return [self updateThread:threadModel dataPushed:dataPushed]; +} + + -(RXPromise *) createThreadWithUsers: (NSArray *) users threadCreated: (void(^)(NSError * error, id thread)) threadCreated { return [self createThreadWithUsers:users name:nil threadCreated:threadCreated]; } + /** * @brief Add users to a thread */ diff --git a/ChatSDKCore/Classes/Hooks/BBaseHookHandler.m b/ChatSDKCore/Classes/Hooks/BBaseHookHandler.m index 1c23cb27..4b54e8f1 100755 --- a/ChatSDKCore/Classes/Hooks/BBaseHookHandler.m +++ b/ChatSDKCore/Classes/Hooks/BBaseHookHandler.m @@ -32,7 +32,7 @@ -(BHook *) addHook: (BHook *) hook withName: (NSString *) name { if(!existingHooks) { existingHooks = [NSMutableArray new]; } - if(![existingHooks containsObject:hook]) { + if(![existingHooks containsObject:hook] && hook != nil) { [existingHooks addObject:hook]; } _hooks[name] = existingHooks; diff --git a/ChatSDKCore/Classes/Session/BConfiguration.h b/ChatSDKCore/Classes/Session/BConfiguration.h index 51320ef4..21a5a452 100755 --- a/ChatSDKCore/Classes/Session/BConfiguration.h +++ b/ChatSDKCore/Classes/Session/BConfiguration.h @@ -153,6 +153,11 @@ @property (nonatomic, readwrite) UIFont * threadTimeFont; @property (nonatomic, readwrite) UIFont * threadSubtitleFont; +@property (nonatomic, readwrite) UIFont * unreadThreadTitleFont; +@property (nonatomic, readwrite) UIFont * unreadThreadTimeFont; +@property (nonatomic, readwrite) UIFont * unreadThreadSubtitleFont; + +@property (nonatomic, readwrite) NSString * threadNameColor; @property (nonatomic, readwrite) BOOL locationMessagesEnabled; @property (nonatomic, readwrite) BOOL imageMessagesEnabled; diff --git a/ChatSDKCore/Classes/UI/NSBundle+Core.h b/ChatSDKCore/Classes/UI/NSBundle+Core.h index 0a8aae24..d0428fea 100755 --- a/ChatSDKCore/Classes/UI/NSBundle+Core.h +++ b/ChatSDKCore/Classes/UI/NSBundle+Core.h @@ -21,13 +21,14 @@ #define bThreadCreationError @"bThreadCreationError" #define bSearchTerm @"bSearchTerm" -#define bPickFriends @"bPickFriends" +#define bPickFriends @"NewChat" +#define bInviteFriend @"Invite" -#define bBack @"bBack" +#define bBack @"Back" #define bImageSaved @"bImageSaved" #define bOpenInMaps @"bOpenInMaps" #define bLocation @"bLocation" -#define bCompose @"bCompose" +#define bCompose @"Create" #define bCreateGroup @"bCreateGroup" #define bLoadingFriends @"bLoadingFriends" @@ -76,11 +77,11 @@ #define bChooseExistingPhoto @"bChooseExistingPhoto" #define bChooseExistingVideo @"bChooseExistingVideo" #define bCurrentLocation @"bCurrentLocation" -#define bSend @"bSend" +#define bSend @"SEND" #define bOpen @"bOpen" #define bReply @"bReply" #define bRec @"bRec" -#define bWriteSomething @"bWriteSomething" +#define bWriteSomething @"type_something" #define bSlideToCancel @"bSlideToCancel" #define bFlagged @"bFlagged" #define bFlag @"bFlag" @@ -101,7 +102,7 @@ #define bInviteBySMS @"bInviteBySMS" #define bTo @"bTo" -#define bEnterNamesHere @"bEnterNamesHere" +#define bEnterNamesHere @"SearchMembers" //@"bEnterNamesHere" #define bSearchedContacts @"bSearchedContacts" #define bNearbyContacts @"bNearbyContacts" @@ -123,8 +124,8 @@ #define bDeletePicture @"bDeletePicture" #define bSetAsDefaultPicture @"bSetAsDefaultPicture" #define bDeleteLastPictureWarning @"bDeleteLastPictureWarning" -#define bDone @"bDone" -#define bEdit @"bEdit" +#define bDone @"Done" +#define bEdit @"Edit" #define b_Ago @"b_Ago" #define bRemoveFriend @"bRemoveFriend" @@ -137,12 +138,12 @@ #define bTermsAndConditions @"bTermsAndConditions" -#define bNoMessages @"bNoMessages" +#define bNoMessages @"no_messages" #define bNoNewUsersFoundForThisSearch @"bNoNewUsersFoundForThisSearch" #define bLastSeen_at_ @"bLastSeen_at_" #define b_at_ @"b_at_" -#define bToday @"Today" -#define bYesterday @"Yesterday" +#define bToday @"today" +#define bYesterday @"yesterday" #define bYouLeftTheGroup @"bYouLeftTheGroup" #define bYouJoinedTheGroup @"bYouJoinedTheGroup" #define bRejoinGroup @"bRejoinGroup" @@ -184,6 +185,10 @@ #define bVideoMessage @"bVideoMessage" #define bStickerMessage @"bStickerMessage" #define bFileMessage @"bFileMessage" +#define bProfilePictures @"bProfilePictures" +#define bDeletePicture @"bDeletePicture" +#define bSetAsDefaultPicture @"bSetAsDefaultPicture" +#define bDeleteLastPictureWarning @"bDeleteLastPictureWarning" #define bAvailable @"bAvailable" #define bAway @"bAway" diff --git a/ChatSDKCore/Classes/UI/NSBundle+Core.h.orig b/ChatSDKCore/Classes/UI/NSBundle+Core.h.orig new file mode 100755 index 00000000..17217bc2 --- /dev/null +++ b/ChatSDKCore/Classes/UI/NSBundle+Core.h.orig @@ -0,0 +1,210 @@ +// +// NSBundle+Core.h +// Pods +// +// Created by Benjamin Smiley-andrews on 12/07/2017. +// +// + +#import + +#define bLogout @"bLogout" +#define bSettings @"bSettings" +#define bAuthenticating @"bAuthenticating" +#define bLogginIn @"bLogginIn" +#define bErrorTitle @"bErrorTitle" +#define bContacts @"bContacts" +#define bConversations @"bConversations" +#define bBroadcast @"bBroadcast" +//#define bThread @"bThread" +#define bChatRooms @"bChatRooms" +#define bThreadCreationError @"bThreadCreationError" +#define bSearchTerm @"bSearchTerm" + +#define bPickFriends @"NewChat" +#define bInviteFriend @"Invite" + +#define bBack @"Back" +#define bImageSaved @"bImageSaved" +#define bOpenInMaps @"bOpenInMaps" +#define bLocation @"bLocation" +#define bCompose @"Create" +#define bCreateGroup @"bCreateGroup" + +#define bLoadingFriends @"bLoadingFriends" +#define bLoading @"bLoading" +#define bErrorLoadingFriends @"bErrorLoadingFriends" + +#define bInvalidSelection @"bInvalidSelection" +#define bSelectAtLeastOneFriend @"bSelectAtLeastOneFriend" + +#define bInviteFriendsFromFacebook @"bInviteFriendsFromFacebook" +#define bFriendsAdded_i @"bFriendsAdded_i" + +#define bSuccess @"bSuccess" +#define bAdded @"bAdded" + +#define bAdd @"bAdd" +#define bCreatingThread @"bCreatingThread" +#define bLogoutErrorTitle @"bLogoutErrorTitle" + +#define bSearch @"bSearch" +#define bSearching @"bSearching" +#define bNoNearbyUsers @"bNoNearbyUsers" + +#define bAddUsers @"bAddUsers" + +#define bOnline @"online" +<<<<<<< HEAD +#define bOffline @"bOffline" +======= +#define bOffline @"offline" +>>>>>>> ba622d8b140a9791dcf57da71313e8792eb58a33 + +#define bCreatePublicThread @"bCreatePublicThread" +#define bThreadName @"bThreadName" +#define bCancel @"bCancel" +#define bOk @"bOk" +#define bReset @"bReset" +#define bLogin @"bLogin" +#define bRegister @"bRegister" +#define bPassword @"bPassword" +#define bForgotPassword @"bForgotPassword" +#define bEnterCredentialToResetPassword @"bEnterCredentialToResetPassword" +#define bPasswordResetSuccess @"bPasswordResetSuccess" +#define bUnableToCreateThread @"bUnableToCreateThread" +#define bChat @"bChat" +#define bOptions @"bOptions" +#define bTakePhoto @"bTakePhoto" +#define bTakeVideo @"bTakeVideo" +#define bTakePhotoOrVideo @"bTakePhotoOrVideo" +#define bChooseExistingPhoto @"bChooseExistingPhoto" +#define bChooseExistingVideo @"bChooseExistingVideo" +#define bCurrentLocation @"bCurrentLocation" +#define bSend @"SEND" +#define bOpen @"bOpen" +#define bReply @"bReply" +#define bRec @"bRec" +#define bWriteSomething @"type_something" +#define bSlideToCancel @"bSlideToCancel" +#define bFlagged @"bFlagged" +#define bFlag @"bFlag" +#define bDelete @"bDelete" +#define bUnflag @"bUnflag" +#define bHoldToSendAudioMessageError @"bHoldToSendAudioMessageError" +#define bRecording @"bRecording" +#define bSecondsRemaining_ @"bSecondsRemaining_" +#define bAudioLengthLimitReached @"bAudioLengthLimitReached" +#define bSendOrDiscardRecording @"bSendOrDiscardRecording" + +#define bCancelled @"bCancelled" +#define bSave @"bSave" +#define bSaving @"bSaving" +#define bGroupName @"bGroupName" +#define bInviteByEmail @"bInviteByEmail" +#define bInviteContact @"bInviteContact" +#define bInviteBySMS @"bInviteBySMS" + +#define bTo @"bTo" +#define bEnterNamesHere @"SearchMembers" //@"bEnterNamesHere" +#define bSearchedContacts @"bSearchedContacts" +#define bNearbyContacts @"bNearbyContacts" + +#define bName @"bName" +#define bPhoneNumber @"bPhoneNumber" +#define bEmail @"bEmail" +#define bDetails @"bDetails" +#define bAddParticipant @"bAddParticipant" +#define bLeaveConversation @"bLeaveConversation" +#define bRejoinConversation @"bRejoinConversation" +#define bParticipants @"bParticipants" +#define bActiveParticipants @"bActiveParticipants" +#define bNoActiveParticipants @"bNoActiveParticipants" +#define bTapHereForContactInfo @"bTapHereForContactInfo" + +#define bProfile @"bProfile" +#define bProfilePictures @"bProfilePictures" +#define bAddPictures @"bAddPicture" +#define bDeletePicture @"bDeletePicture" +#define bSetAsDefaultPicture @"bSetAsDefaultPicture" +#define bDeleteLastPictureWarning @"bDeleteLastPictureWarning" +#define bDone @"Done" +#define bEdit @"Edit" +#define b_Ago @"b_Ago" + +#define bRemoveFriend @"bRemoveFriend" +#define bAddFriend @"bAddFriend" +#define bAddContact @"bAddContact" +#define bUnblock @"bUnblock" +#define bBlock @"bBlock" +#define b_LeftTheGroup @"b_LeftTheGroup" +#define b_JoinedTheGroup @"b_JoinedTheGroup" + +#define bTermsAndConditions @"bTermsAndConditions" + +#define bNoMessages @"no_messages" +#define bNoNewUsersFoundForThisSearch @"bNoNewUsersFoundForThisSearch" +#define bLastSeen_at_ @"bLastSeen_at_" +#define b_at_ @"b_at_" +#define bToday @"today" +#define bYesterday @"yesterday" +#define bYouLeftTheGroup @"bYouLeftTheGroup" +#define bYouJoinedTheGroup @"bYouJoinedTheGroup" +#define bRejoinGroup @"bRejoinGroup" + +#define bDefaultThreadName @"bDefaultThreadName" + +#define bDeleteContact @"bDeleteContact" +#define bDeleteContactMessage @"bDeleteContactMessage" + +#define bTyping @"bTyping" +#define bCamera @"bCamera" +#define bPhoto @"bPhoto" +#define bChoosePhoto @"bChoosePhoto" +#define bChooseVideo @"bChooseVideo" +#define bSticker @"bSticker" +#define bFile @"bFile" +#define bRefreshingUsers @"bRefreshingUsers" +#define bMessageBurst @"bMessageBurst" + +#define bSelectYourSearch @"bSelectYourSearch" +#define bPhonebook @"bPhonebook" +#define bSearchWithName @"bSearchWithName" +#define bWarning @"bWarning" +#define bYouMustEnableContactPermissionsToUseThisFunctionalityEnableInTheSettingsApp @"bYouMustEnableContactPermissionsToUseThisFunctionalityEnableInTheSettingsApp" + +#define bImageMessagesNotSupported @"bImageMessagesNotSupported" +#define bAudioMessagesNotSupported @"bAudioMessagesNotSupported" +#define bStickerMessagesNotSupported @"bStickerMessagesNotSupported" +#define bFileMessagesNotSupported @"bFileMessagesNotSupported" +#define bLocationMessagesNotSupported @"bLocationMessagesNotSupported" +#define bVideoMessagesNotSupported @"bVideoMessagesNotSupported" + +#define bBlock @"bBlock" +#define bUnblock @"bUnblock" + +#define bImageMessage @"bImageMessage" +#define bLocationMessage @"bLocationMessage" +#define bAudioMessage @"bAudioMessage" +#define bVideoMessage @"bVideoMessage" +#define bStickerMessage @"bStickerMessage" +#define bFileMessage @"bFileMessage" +#define bProfilePictures @"bProfilePictures" +#define bDeletePicture @"bDeletePicture" +#define bSetAsDefaultPicture @"bSetAsDefaultPicture" +#define bDeleteLastPictureWarning @"bDeleteLastPictureWarning" + +#define bAvailable @"bAvailable" +#define bAway @"bAway" +#define bExtendedAway @"bExtendedAway" +#define bBusy @"bBusy" + +@protocol PMessage; + +@interface NSBundle(ChatCore) + ++(NSBundle *) coreBundle; ++(NSString *) t: (NSString *) string; ++(NSString *) textForMessage: (id) message; + +@end diff --git a/ChatSDKCore/LICENSE b/ChatSDKCore/LICENSE deleted file mode 100755 index 04aa4731..00000000 --- a/ChatSDKCore/LICENSE +++ /dev/null @@ -1,53 +0,0 @@ -Chat SDK License - -We offer a choice of two license for this app. You can either use the [Chat SDK](https://chatsdk.co/chat-sdk-license/) license or the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) license. - -### Chat SDK License Summary - -+ License does not expire. -+ Can be used for creating unlimited applications -+ Can be distributed in binary or object form only -+ Commercial use allowed -+ Can modify source-code but cannot distribute modifications (derivative works) - -### GPLv3 License Summary - -+ Can modify and distribute source code -+ Commerical use allowed -+ Cannot sublicense or hold liable -+ Must include original license -+ Must disclose source - -### FAQ - -1. **I want to release an app to the App Store. Do I have to pay anything?** - - _When you release an app on the app store, you are releasing a compiled app. This is the most common use case for our users. The Chat SDK license allows you to release unlimited commerical apps on the app store without paying anything._ - -2. **Do I have to include any attribution with my binary?** - - _No. Attribution is not required._ - -3. **Do I have to include a license file with my binary?** - - _No. It is not necessary to include a license file with your binary._ - -4. **I'm releasing a commercial app. Do I need to pay anything?** - - _If you are releasing the app in binary form, then commercial use is allowed and you do not need to pay anything._ - -5. **Can I release the Chat SDK straight onto the App Store with no modifications?** - - _Yes. You can release the code in binary form with or without modifications._ - -6. **Do I need to get any kind of permission from you before releasing my app?** - - _No. It is not necessary to ask permission before releasing your app under the Chat SDK license._ - -7. **I have an open source project, can I include the Chat SDK?** - - _If your project license is compatible with the GPLv3 license then you can. If not, you should email us and we will come up with a custom licensing scheme. The price will depend on the circumstances. We will often allow the code to be used for free for non-commerical projects._ - -8. **Why do you have a dual licensing scheme?** - - _We want to give our users as much flexibility as possible while retaining some degree of control over our code. Imageing that someone decided to make some minor modificaitons and started selling our code on a marketplace. This wouldn't benefit anyone because we want to make the code available for free. The dual licensing scheme allows you to do pretty much anything apart from sell our source code directly._ \ No newline at end of file diff --git a/ChatSDKCoreData/Assets/.gitkeep b/ChatSDKCoreData/Assets/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/ChatSDKCoreData/Classes/.gitkeep b/ChatSDKCoreData/Classes/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/ChatSDKCoreData/LICENSE b/ChatSDKCoreData/LICENSE deleted file mode 100755 index 04aa4731..00000000 --- a/ChatSDKCoreData/LICENSE +++ /dev/null @@ -1,53 +0,0 @@ -Chat SDK License - -We offer a choice of two license for this app. You can either use the [Chat SDK](https://chatsdk.co/chat-sdk-license/) license or the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) license. - -### Chat SDK License Summary - -+ License does not expire. -+ Can be used for creating unlimited applications -+ Can be distributed in binary or object form only -+ Commercial use allowed -+ Can modify source-code but cannot distribute modifications (derivative works) - -### GPLv3 License Summary - -+ Can modify and distribute source code -+ Commerical use allowed -+ Cannot sublicense or hold liable -+ Must include original license -+ Must disclose source - -### FAQ - -1. **I want to release an app to the App Store. Do I have to pay anything?** - - _When you release an app on the app store, you are releasing a compiled app. This is the most common use case for our users. The Chat SDK license allows you to release unlimited commerical apps on the app store without paying anything._ - -2. **Do I have to include any attribution with my binary?** - - _No. Attribution is not required._ - -3. **Do I have to include a license file with my binary?** - - _No. It is not necessary to include a license file with your binary._ - -4. **I'm releasing a commercial app. Do I need to pay anything?** - - _If you are releasing the app in binary form, then commercial use is allowed and you do not need to pay anything._ - -5. **Can I release the Chat SDK straight onto the App Store with no modifications?** - - _Yes. You can release the code in binary form with or without modifications._ - -6. **Do I need to get any kind of permission from you before releasing my app?** - - _No. It is not necessary to ask permission before releasing your app under the Chat SDK license._ - -7. **I have an open source project, can I include the Chat SDK?** - - _If your project license is compatible with the GPLv3 license then you can. If not, you should email us and we will come up with a custom licensing scheme. The price will depend on the circumstances. We will often allow the code to be used for free for non-commerical projects._ - -8. **Why do you have a dual licensing scheme?** - - _We want to give our users as much flexibility as possible while retaining some degree of control over our code. Imageing that someone decided to make some minor modificaitons and started selling our code on a marketplace. This wouldn't benefit anyone because we want to make the code available for free. The dual licensing scheme allows you to do pretty much anything apart from sell our source code directly._ \ No newline at end of file diff --git a/ChatSDKExtras/SideMenu/Classes/Extras.h b/ChatSDKExtras/SideMenu/Classes/Extras.h deleted file mode 100755 index 46a51554..00000000 --- a/ChatSDKExtras/SideMenu/Classes/Extras.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// Extras.h -// ChatSDK Demo -// -// Created by Ben on 9/17/18. -// Copyright © 2018 deluge. All rights reserved. -// - -#ifndef Extras_h -#define Extras_h - - -#endif /* Extras_h */ diff --git a/ChatSDKExtras/SideMenu/Interface/SideMenu.storyboard b/ChatSDKExtras/SideMenu/Interface/SideMenu.storyboard deleted file mode 100755 index 49eda506..00000000 --- a/ChatSDKExtras/SideMenu/Interface/SideMenu.storyboard +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Firebase/CCThreadWrapper.m b/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Firebase/CCThreadWrapper.m index 34849041..a5c3476e 100755 --- a/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Firebase/CCThreadWrapper.m +++ b/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Firebase/CCThreadWrapper.m @@ -564,6 +564,7 @@ -(RXPromise *) push { return promise; } + -(RXPromise *) addUserWithEntityID: (NSString *) entityID { FIRDatabaseReference * threadUsersRef = [[FIRDatabaseReference threadUsersRef:_model.entityID] child:entityID]; diff --git a/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseContactHandler.m b/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseContactHandler.m index 1b186100..f6adefa7 100755 --- a/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseContactHandler.m +++ b/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseContactHandler.m @@ -12,7 +12,9 @@ @implementation BFirebaseContactHandler -(RXPromise *) addContact: (id) contact withType: (bUserConnectionType) type { RXPromise * promise = [RXPromise new]; - + if (!BChatSDK.currentUserID) { + return promise; + } FIRDatabaseReference * ref = [[FIRDatabaseReference userContactsRef:BChatSDK.currentUserID] child:contact.entityID]; [ref setValue:@{bType: @(type)} withCompletionBlock:^(NSError * error, FIRDatabaseReference * ref) { if (!error) { diff --git a/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseCoreHandler.m b/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseCoreHandler.m index 5d52a8c0..07de6d62 100755 --- a/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseCoreHandler.m +++ b/ChatSDKFirebase/FirebaseNetworkAdapter/Classes/Handlers/BFirebaseCoreHandler.m @@ -51,6 +51,7 @@ -(void) goOffline { [FIRDatabaseReference goOffline]; } + -(RXPromise *)observeUser: (NSString *)entityID { id userModel = [BChatSDK.db fetchOrCreateEntityWithID:entityID withType:bUserEntity]; [[CCUserWrapper userWithModel:userModel] onlineOn]; @@ -91,6 +92,25 @@ -(RXPromise *) createThreadWithUsers: (NSArray *) users } } +-(RXPromise *) updateThread: (id) threadModel dataPushed: (void(^)(NSError * error, id thread)) dataPushed { + CCThreadWrapper * thread = [CCThreadWrapper threadWithModel:threadModel]; + + return [thread push].thenOnMain(^id(id thread) { + + if (dataPushed != Nil) { + dataPushed(Nil, thread); + } + },^id(NSError * error) { + //[BChatSDK.db undo]; + + if (dataPushed != Nil) { + dataPushed(error, Nil); + } + return error; + }); + +} + -(RXPromise *) addUsers: (NSArray *) users toThread: (id) threadModel { CCThreadWrapper * thread = [CCThreadWrapper threadWithModel:threadModel]; diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Assets/BGoogleLoginViewController.xib b/ChatSDKFirebase/FirebaseSocialLogin/Assets/BGoogleLoginViewController.xib deleted file mode 100755 index 6ce64f59..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Assets/BGoogleLoginViewController.xib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Assets/icn_200_google@3x.png b/ChatSDKFirebase/FirebaseSocialLogin/Assets/icn_200_google@3x.png deleted file mode 100755 index 5bf6170a..00000000 Binary files a/ChatSDKFirebase/FirebaseSocialLogin/Assets/icn_200_google@3x.png and /dev/null differ diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginHandler.h b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginHandler.h deleted file mode 100755 index b3049b94..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginHandler.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// BSocialLoginHandler.h -// Pods -// -// Created by Benjamin Smiley-andrews on 01/03/2017. -// -// - -#import -#import - -@interface BFirebaseSocialLoginHandler : NSObject { -} - - - -@end diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginHandler.m b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginHandler.m deleted file mode 100755 index c7b5ee25..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginHandler.m +++ /dev/null @@ -1,140 +0,0 @@ -// -// BSocialLoginHandler.m -// Pods -// -// Created by Benjamin Smiley-andrews on 01/03/2017. -// -// - -#import "BFirebaseSocialLoginHandler.h" - -#import - -#import -#import - -#import - -#import "BGoogleHelper.h" - -@import TwitterKit; - - -@implementation BFirebaseSocialLoginHandler - --(id) init { - if((self = [super init])) { - [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:Nil queue:0 usingBlock:^(NSNotification * notificaiton) { - [FBSDKAppEvents activateApp]; - }]; - } - return self; -} - --(void) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - - [[FBSDKApplicationDelegate sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; - - [[Twitter sharedInstance] startWithConsumerKey:BChatSDK.config.twitterApiKey - consumerSecret:BChatSDK.config.twitterSecret]; - -} - --(BOOL) application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - if ([[url scheme] hasPrefix:@"fb"]) { - return [[FBSDKApplicationDelegate sharedInstance] application:app openURL:url options:options]; - } - else if ([[url scheme] hasPrefix:@"twitterkit"]) { - return [[Twitter sharedInstance] application:app openURL:url options:options]; - } - return NO; -} - -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication - annotation:(id)annotation { - if ([[url scheme] hasPrefix:@"fb"]) { - - return [[FBSDKApplicationDelegate sharedInstance] application:application - openURL:url - sourceApplication:sourceApplication - annotation:annotation]; - } - if ([[url scheme] hasPrefix:@"twitterkit"]) { - return YES; - } - else { - return [[GIDSignIn sharedInstance] handleURL:url]; -// return [[GIDSignIn sharedInstance] handleURL:url sourceApplication:application annotation:annotation]; - } -} - --(RXPromise *) loginWithGoogle { - - RXPromise * promise = [RXPromise new]; - - BGoogleHelper * googleHelper = [[BGoogleHelper alloc] init]; - - [googleHelper loginWithGoogle].thenOnMain(^id(id success) { - - GIDAuthentication * authentication = [GIDSignIn sharedInstance].currentUser.authentication; - [promise resolveWithResult:@[authentication.idToken, authentication.accessToken]]; - - return Nil; - }, ^id(NSError * error) { - [promise rejectWithReason:error]; - return Nil; - }); - - return promise; -} - --(RXPromise *) loginWithTwitter { - - RXPromise * promise = [RXPromise new]; - - [[Twitter sharedInstance] logInWithCompletion:^(TWTRSession *session, NSError *error) { - if (!error) { - [promise resolveWithResult:@[session.authToken, session.authTokenSecret]]; - } - else { - [promise rejectWithReason:error]; - } - }]; - return promise; - -} - --(RXPromise *) loginWithFacebook { - - RXPromise * promise = [RXPromise new]; - - if([FBSDKAccessToken currentAccessToken]) { - [promise resolveWithResult:[FBSDKAccessToken currentAccessToken].tokenString]; - } - else { - - // TODO: Check this - FBSDKLoginManager * manager = [[FBSDKLoginManager alloc] init]; - - [manager logInWithPermissions:@[@"public_profile", @"email"] fromViewController: Nil handler:^(FBSDKLoginManagerLoginResult * result, NSError * error) { - if(!error && [FBSDKAccessToken currentAccessToken].tokenString != Nil) { - [promise resolveWithResult:[FBSDKAccessToken currentAccessToken].tokenString]; - } - else { - [promise rejectWithReason:error]; - } - }]; - } - - return promise; - -} - -- (void)applicationDidBecomeActive:(UIApplication *)application { - -} - - -@end diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginModule.h b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginModule.h deleted file mode 100755 index 99249504..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginModule.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// BFirebaseSocialLoginModule.h -// ChatSDK Demo -// -// Created by Ben on 8/29/17. -// Copyright © 2017 deluge. All rights reserved. -// - -#import -#import -#import - -@interface BFirebaseSocialLoginModule : NSObject - --(void) activate; - -@end diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginModule.m b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginModule.m deleted file mode 100755 index 21bd6a3e..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BFirebaseSocialLoginModule.m +++ /dev/null @@ -1,19 +0,0 @@ -// -// BFirebaseSocialLoginModule.m -// ChatSDK Demo -// -// Created by Ben on 8/29/17. -// Copyright © 2017 deluge. All rights reserved. -// - -#import "BFirebaseSocialLoginModule.h" -#import "SocialLogin.h" -#import - -@implementation BFirebaseSocialLoginModule - --(void) activate { - BChatSDK.shared.networkAdapter.socialLogin = [[BFirebaseSocialLoginHandler alloc] init]; -} - -@end diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleHelper.h b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleHelper.h deleted file mode 100755 index 192bd8fe..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleHelper.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// BGoogleHelper.h -// Pods -// -// Created by Simon Smiley-Andrews on 29/03/2017. -// -// - -#import -#import "PGoogleLoginDelegate.h" - -@class RXPromise; - -@interface BGoogleHelper : NSObject { - RXPromise * _promise; -} - -- (RXPromise *)loginWithGoogle; - -@end diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleHelper.m b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleHelper.m deleted file mode 100755 index 1d881278..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleHelper.m +++ /dev/null @@ -1,53 +0,0 @@ -// -// BGoogleHelper.m -// Pods -// -// Created by Simon Smiley-Andrews on 29/03/2017. -// -// - -#import "BGoogleHelper.h" - -#import - -@implementation BGoogleHelper - -// Call this to login with Google -- (RXPromise *)loginWithGoogle { - - if (!_promise) { - _promise = [RXPromise new]; - - BGoogleLoginViewController * vc = [[BGoogleLoginViewController alloc] init]; - vc.delegate = self; - - UIWindow * window = [UIApplication sharedApplication].keyWindow; - UIViewController * rootViewController = window.rootViewController; - - // Dismiss the login view - [rootViewController dismissViewControllerAnimated:NO completion:^{ - [rootViewController presentViewController:vc animated:YES completion:nil]; - }]; - } - else { - return [RXPromise rejectWithReasonDomain:@"" code:0 description:@"Google login already in progress"]; - } - - return _promise; -} - --(void) loginWasSuccessful { - if (_promise) { - [_promise resolveWithResult:Nil]; - _promise = Nil; - } -} - --(void) loginFailedWithError: (NSError *) error { - if (_promise) { - [_promise rejectWithReason:error]; - _promise = Nil; - } -} - -@end diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleLoginViewController.h b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleLoginViewController.h deleted file mode 100755 index 4e8acfe7..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleLoginViewController.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// BGoogleLoginViewController.h -// Pods -// -// Created by Simon Smiley-Andrews on 03/03/2017. -// -// - -#import - -#import "PGoogleLoginDelegate.h" -#import - -@interface BGoogleLoginViewController : UIViewController - -@property (nonatomic, strong) id delegate; - - -@end - diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleLoginViewController.m b/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleLoginViewController.m deleted file mode 100755 index 107aaed1..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/BGoogleLoginViewController.m +++ /dev/null @@ -1,86 +0,0 @@ -// -// BGoogleLoginViewController.m -// Pods -// -// Created by Simon Smiley-Andrews on 03/03/2017. -// -// - -#import "BGoogleLoginViewController.h" - -#import -#import - -@interface BGoogleLoginViewController () - -@end - -@implementation BGoogleLoginViewController - -@synthesize delegate; - -- (id)init { - - NSBundle * bundle = [NSBundle bundleWithName:bSocialLoginBundleName]; - - self = [super initWithNibName:@"BGoogleLoginViewController" bundle:bundle]; - if (self) { - - } - return self; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - UIImageView * googleLogo = [[UIImageView alloc] initWithImage: [UIImage imageNamed:@"icn_200_google.png"]]; - [self.view addSubview:googleLogo]; - - googleLogo.keepHorizontalCenter.equal = 0.5; - googleLogo.keepHeight.equal = 200; - googleLogo.keepWidth.equal = 200; - googleLogo.keepVerticalCenter.equal = 0.5; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear: animated]; - - [GIDSignIn sharedInstance].delegate = self; - [GIDSignIn sharedInstance].clientID = BChatSDK.config.googleClientKey; - - [[GIDSignIn sharedInstance] setScopes:@[@"https://www.googleapis.com/auth/plus.login", @"https://www.googleapis.com/auth/plus.me"]]; -} - -- (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; - - if (![[GIDSignIn sharedInstance] currentUser]) { - [[GIDSignIn sharedInstance] signIn]; - } - else { - - [self dismissViewControllerAnimated:NO completion:^{ - if (delegate) { - [delegate loginWasSuccessful]; - delegate = Nil; - } - }]; - } -} - -// Implement the required GIDSignInDelegate methods -- (void)signIn:(GIDSignIn *)signIn didSignInForUser:(GIDGoogleUser *)user withError:(NSError *)error { - - [self dismissViewControllerAnimated:NO completion:^{ - if (delegate) { - if (error) { - [delegate loginFailedWithError:error]; - } - else { - [delegate loginWasSuccessful]; - } - delegate = Nil; - } - }]; -} - -@end diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/PGoogleLoginDelegate.h b/ChatSDKFirebase/FirebaseSocialLogin/Classes/PGoogleLoginDelegate.h deleted file mode 100755 index 19b20293..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/PGoogleLoginDelegate.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// PGoogleLoginDelegate.h -// Pods -// -// Created by Simon Smiley-Andrews on 29/03/2017. -// -// - -#ifndef PGoogleLoginDelegate_h -#define PGoogleLoginDelegate_h - -@class RXPromise; - -@protocol PGoogleLoginDelegate - --(void) loginWasSuccessful; --(void) loginFailedWithError: (NSError *) error; - -@end - - -#endif /* PGoogleLoginDelegate_h */ diff --git a/ChatSDKFirebase/FirebaseSocialLogin/Classes/SocialLogin.h b/ChatSDKFirebase/FirebaseSocialLogin/Classes/SocialLogin.h deleted file mode 100755 index be13b6d9..00000000 --- a/ChatSDKFirebase/FirebaseSocialLogin/Classes/SocialLogin.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// SocialLogin.h -// Pods -// -// Created by Benjamin Smiley-andrews on 01/03/2017. -// -// - -#ifndef SocialLogin_h -#define SocialLogin_h - -#define ChatSDKSocialLoginModule - -#define bSocialLoginBundleName @"ChatFirebaseSocialLogin" - -#import -#import "BFirebaseSocialLoginHandler.h" -#import "BGoogleLoginViewController.h" - -#endif /* SocialLogin_h */ diff --git a/ChatSDKFirebase/FirebaseUI/classes/BFirebaseUIModule.h b/ChatSDKFirebase/FirebaseUI/classes/BFirebaseUIModule.h deleted file mode 100755 index a11ebc95..00000000 --- a/ChatSDKFirebase/FirebaseUI/classes/BFirebaseUIModule.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// BFirebaseUIModule.h -// ChatSDKSwift -// -// Created by Ben on 8/30/17. -// Copyright © 2017 deluge. All rights reserved. -// - -#import - -@import FirebaseUI; - -@protocol FUIAuthDelegate; - -@protocol BFirebaseUIModuleDelegate - --(void) authCompletedWithError: (NSError *) error; - -@end - -@interface BFirebaseUIModule : NSObject - --(void) activateWithProviders: (NSArray *) providers; --(FUIAuthPickerViewController *) viewControllerForProviders: (NSArray *) providers; - -@property (nonatomic, readwrite, weak) id delegate; - -@end diff --git a/ChatSDKFirebase/FirebaseUI/classes/BFirebaseUIModule.m b/ChatSDKFirebase/FirebaseUI/classes/BFirebaseUIModule.m deleted file mode 100755 index 115b8e1c..00000000 --- a/ChatSDKFirebase/FirebaseUI/classes/BFirebaseUIModule.m +++ /dev/null @@ -1,57 +0,0 @@ -// -// BFirebaseUIModule.m -// ChatSDKSwift -// -// Created by Ben on 8/30/17. -// Copyright © 2017 deluge. All rights reserved. -// - -#import "BFirebaseUIModule.h" -#import -#import -//#import -#import - -@implementation BFirebaseUIModule - --(FUIAuthPickerViewController *) viewControllerForProviders: (NSArray *) providers { - FIRAuth * auth = [FIRAuth auth]; - FUIAuth * authUI = [FUIAuth authUIWithAuth:auth]; - - // This allows us to be notified when authentication finishes - authUI.delegate = self; - - // Add the phone provider - [authUI setProviders:providers]; - - return [[FUIAuthPickerViewController alloc] initWithAuthUI:authUI]; -} - --(void) activateWithProviders: (NSArray *) providers { - BChatSDK.ui.loginViewController = [self viewControllerForProviders:providers]; -} - -- (void)authUI:(FUIAuth *)authUI didSignInWithAuthDataResult:(FIRAuthDataResult *)authDataResult error:(NSError *)error { - if (!error) { - - [BChatSDK.auth authenticate].thenOnMain(^id(id user) { - [self notifyDelegate:Nil]; - return Nil; - }, ^id(NSError * error) { - [self notifyDelegate:error]; - return Nil; - }); - } - else { - [self notifyDelegate:error]; - } -} - --(void) notifyDelegate: (NSError *) error { - if(self.delegate != Nil) { - [self.delegate authCompletedWithError:error]; - } -} - - -@end diff --git a/ChatSDKKeepLayout/LICENSE.md b/ChatSDKKeepLayout/LICENSE.md new file mode 100644 index 00000000..195e608e --- /dev/null +++ b/ChatSDKKeepLayout/LICENSE.md @@ -0,0 +1,7 @@ +**Copyright © 2013-2016 Martin Kiss** + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +_The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software._ + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPL IED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ChatSDKKeepLayout/README.md b/ChatSDKKeepLayout/README.md new file mode 100644 index 00000000..b71652dc --- /dev/null +++ b/ChatSDKKeepLayout/README.md @@ -0,0 +1,298 @@ +# Keep Layout Flattr this + +Keep Layout makes _Auto Layout_ much easier to use _from code_! No more _Interface Builder_ or _Visual Format_. _Keep Layout_ provides **simple, more readable and powerful API for creating and _accessing existing_ constraints**. + + +Before you start, you should be familiar with _Auto Layout_ topic. [How it works and what's the point?][1] + + +_**Project Status**: All planned features are implemented and functional. Project is maintained and kept up to date with latest iOS/OS X. Feel free to submit Issues or Pull Requests._ + +_**Swift Adopters**: Swift is fully supported, the usage syntax is the mostly the same._ + +_**Origins:** This library was originally made for [this app](https://itunes.apple.com/app/geography-of-the-world/id391081388?mt=8&ct=KeepLayout), especially for its iPad interface._ + + +## Attributes + +Every view has several _attributes_ that are represented by `KeepAttribute` class. + + - Dimensions: **width**, **height**, **aspect ratio** + - Insets to superview: **top**, **bottom**, **left**, **right** + - Insets to superview margins: **top**, **bottom**, **left**, **right** + - Position in superview: **horizontal** and **vertical** + - Offsets to other views: **top**, **bottom**, **left**, **right** + - Alignments with other views: **top**, **bottom**, **left**, **right**, **horizontal**, **vertical**, **first & last baselines** + +They can be accessed by calling methods on `UIView`/`NSView` object with one of these format: + +```objc +@property (readonly) KeepLayout *keep; +@property (readonly) KeepLayout *(^keepTo)(UIView *)); // Returns block taking another view. +``` + +Example: + +```objc +KeepAttribute *width = view.keepWidth; +KeepAttribute *topOffset = view.keepTopOffsetTo(anotherView); // Invoking the block that returns the actual attribute. +``` + +Calling such method for the first time creates the attribute object and any subsequent calls will return the same object. For attributes related to other views this is true for each pair of views. Sometimes even in inversed order or direction: + +```objc +// aligns are the same regardless of order +viewOne.keepLeftAlign(viewTwo) == viewTwo.keepLeftAlign(viewOne) +// left offset from 1 to 2 is right offset from 2 to 1 +viewOne.keepLeftOffset(viewTwo) == viewTwo.keepRightOffset(viewOne) +``` + +See [`KeepView.h`][2] for more. + + + +## Values + +Attributes have three properties: **equal**, **min** and **max**. These are not just plain scalar values, but may have associated a **priority**. + +Priority is _Required_ by default, but you can specify arbitrary priority using provided macros: + +```objc +KeepValue value = 42; // value 42 at priority 1000 +value = 42 +keepHigh; // value 42 at priority 750 +value = 42 +keepLow; // value 42 at priority 250 +value = 42 +keepFitting; // value 42 at priority 50 + +// Arbitrary priority: +value = 42 +keepAt(800); // value 42 at priority 800 +``` + +Priorities are redeclared as `KeepPriority` using `UILayoutPriority` values and they use similar naming: + +```objc +Required > High > Low > Fitting +1000 750 250 50 +``` + +See [`KeepTypes.h`][3] for more. + + + +## Examples + +Keep width of the view to be equal to 150: + +```objc +view.keepWidth.equal = 150; +``` + +Keep top inset to superview of the view to be at least 10: + +```objc +view.keepTopInset.min = 10; +``` + +Don't let the first view to get closer than 10 to the second from the left: + +```objc +firstView.keepLeftOffsetTo(secondView).min = 10; +``` + +#### See the _Examples_ app included in the project for more. + + + +--- + + + +## Grouped Attributes + +You will often want to set multiple attributes to the same value. For this we have **grouped attributes**. + +You can create groups at your own: + +```objc +KeepAttribute *leftInsets = [KeepAttribute group: + viewOne.keepLeftInset, + viewTwo.keepLeftInset, + viewThree.keepLeftInset, + nil]; +leftInsets.equal = 10; +``` + +However there are already some accessors to some of them: + +```objc +view.keepSize // group of both Width and Height +view.keepInsets // group of all four insets +view.keepCenter // group of both axis of position +view.keepEdgeAlignTo // group of alignments to all four edges +``` + +See [`KeepView.h`][2] or [`KeepAttribute.h`][4] for more . + + + +## Atomic Groups + +_Atomic Groups_ are a way to deactivate multiple attributes at once. With atomic group you can quickly change one desired set of constraints (= layout) to another. + +```objc +// Create atomic group +KeepAtomic *layout = [KeepAtomic layout:^{ + self.view.keepWidth.min = 320 +keepHigh; // Set minimum width limit. + self.view.keepVerticalInsets.equal = 0; // Vertically stretch to fit. +}]; + +[layout deactivate]; +// self.view no longer has minimum width of 320 and is no longer stretched vertically. +``` + +You can also deactivate them manually using: + +```objc +self.view.keepWidth.min = KeepNone; // Removes minimum width constraint. +[self.view.keepWidth deactivate]; // Removes all constraints for width. +``` + +See [`KeepAttribute.h`][4] for details. + + +## Convenience Methods + +For the most used cases there are convenience methods. Nothing you could write yourself, but simplify your code and improve readability. Some of them: + +```objc +[view keepSize:CGSizeMake(100, 200)]; +[view keepInsets:UIEdgeInsetsMake(10, 20, 30, 40)]; +[view keepCentered]; +``` + +See [`KeepView.h`][2] for more. + + + +## Array Attributes – _What?_ + +Most of the methods added to `UIView`/`NSView` class can also be called on any **array on views**. Such call creates grouped attribute of all contained view attributes: + +```objc +NSArray *views = @[ viewOne, viewTwo, viewThree ]; +views.keepInsets.min = 10; +``` + +**The above code creates and configures 12 layout constraints!** + +In addition, arrays allow you to use related attributes more easily, using another convenience methods: + +```objc +NSArray *views = @[ viewOne, viewTwo, viewThree ]; +[views keepWidthsEqual]; +[views keepHorizontalOffsets:20]; +[views keepTopAligned]; +``` + +You just created 6 new layout constraints, did you notice? + +See [`NSArray+KeepLayout.h`][5] for more. + + + +## Animations + +Constraints can be animated. You can use simple `UIView` block animation, but you need to call `-layoutIfNeeded` at the end of animation block. That triggers `-layoutSubviews` which applies new constraints. + +Or you can use one of the provided methods so you don't need to care: + +```objc +view.keepWidth.equal = 100; + +[view.superview keepAnimatedWithDuration:1 layout:^{ + view.keepWidth.equal = 200; +}]; +``` + +These are instance methods and must be called on parent view of all affected subviews. At the end of layout block this view receives `-layoutIfNeeded` method. Any changes to views out of the receiver's subview tree will not be animated. + +Spring animation from iOS 7 included, animations on OS X are not supported yet. + +See [`KeepView.h`][2] for more. + + + +## Layout Guides + +KeepLayout adds lazy-loaded invisible `.keepLayoutView` to every `UIViewController` in a category. This view is aligned with Top & Bottom Layout Guide and Left & Right Margins of the view controller, which means its size represents visible portion of the view controller. You can use this Layout View to align your views with translucent bars (navigation bar, toolbar, status bar or tab bar). + +```objc +imageView.keepEdgeAlignTo(controller.keepLayoutView).equal = 0; +// imageView will not be covered by UINavigationBar or UITabBar +``` + +See [`UIViewController+KeepLayout.h`][11] for more. + + + +## Debugging + +Keep Layout uses its own `NSLayoutConstraint` subclass that overrides `-debugDescription` method. Once you get error message **_`Unable to simultaneously satisfy constraints.`_**, you will see nicely readable description of every constraint you have created. Example: + +```objc +" to equal to 20 with required priority>", +" to equal to 50 with required priority>", +``` + +With this you can very easily find the wrong attribute and fix it. + +See [`KeepLayoutConstraint.h`][10] for details. + +--- + + +## Implementation Details + +Once the attribute is accessed it is created and associated with given view (runtime asociation). In case of related attribbutes, the second view is used as weak key in `NSMapTable`. +See [`UIView+KeepLayout.m`][6] for details. + +`KeepValue` is declared as `_Complex double`, which allows seamless convertibility from and to `double`. The priority is stored as imaginary part. +See [`KeepTypes.h`][3] for details. + +Each attribute manages up to three constraints (`NSLayoutConstraint`) that are created, updated and removed when needed. One constraint for each of three relations (`NSLayoutRelation` enum) and setting `equal`, `min` or `max` properties modifies them. +See [`KeepAttribute.m`][7] for details. + +`KeepAttribute` class is a class cluster with specific subclasses. One that manages constraints using `constant` value, one for constraints using `multiplier` and one grouping subclass that forwards primitive methods to its contained children. +See [`KeepAttribute.m`][7] for details. + +Array methods usually call the same selector on contained views and return group of returned attributes. +See [`NSArray+KeepLayout.m`][8] for details. + +Animation delay is implemented as real execution delay, not just delay for animating the changes. This differs from `UIView` block animations and allows you to set up animations in the same update cycle as your initial layout. +See [`KeepView.m`][6] for details. + + + +--- +_Version 1.7.0_ + +MIT License, Copyright © 2013-2016 Martin Kiss + +`THE SOFTWARE IS PROVIDED "AS IS", and so on...` see [`LICENSE.md`][9] more. + + + + + +[1]: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG + +[2]: Sources/KeepView.h +[3]: Sources/KeepTypes.h +[4]: Sources/KeepAttribute.h +[5]: Sources/NSArray+KeepLayout.h +[10]: Sources/KeepLayoutConstraint.h +[11]: Sources/UIViewController+KeepLayout.h + +[6]: Sources/KeepView.m +[7]: Sources/KeepAttribute.m +[8]: Sources/NSArray+KeepLayout.m +[9]: LICENSE.md diff --git a/ChatSDKKeepLayout/Sources/KeepArray.h b/ChatSDKKeepLayout/Sources/KeepArray.h new file mode 100644 index 00000000..6fcf23c1 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepArray.h @@ -0,0 +1,429 @@ +// +// KeepArray.h +// Keep Layout +// +// Created by Martin Kiss on 23.6.13. +// Copyright (c) 2013 Triceratops. All rights reserved. +// + +#import "KeepTypes.h" +#import "KeepView.h" + +@class KeepAttribute; + + + + + +/** + Provides similar methods than KPView. Works only on arrays of UIViews/NSViews. For method descriptions see method in KeepView.h with the same name. + + Most of the methods invokes the same selector on contained views and returns group proxy attribute. Setting values of this group will set attributes to all attributes in the group. + + In addition, for every relative attribute there is convenience method, that applies on views in the array in the order. + **/ +@interface NSArray (KeepLayout) + + + + + +#pragma mark - +#pragma mark Dimensions: Grouped + +/// Grouped attribute for Width of contained views. +@property (readonly) KeepAttribute *keepWidth; + +/// Grouped attribute for Height of contained views. +@property (readonly) KeepAttribute *keepHeight; + +/// Grouped attribute for Size (Width + Height) of contained views. +@property (readonly) KeepAttribute *keepSize; + +/// Grouped attribute for Aspect Ratio of contained views. +@property (readonly) KeepAttribute *keepAspectRatio; + +/// Grouped attribute for Relative Width of contained views. +@property (readonly) KeepRelatedAttributeBlock keepWidthTo; + +/// Grouped attribute for Relative Height of contained views. +@property (readonly) KeepRelatedAttributeBlock keepHeightTo; + +/// Grouped attribute for Relative Size of contained views. +@property (readonly) KeepRelatedAttributeBlock keepSizeTo; + + + +#pragma mark Dimensions: Forwarded + +/// Forwards to contained views. +- (void)keepSize:(CGSize)size priority:(KeepPriority)priority; + +/// Forwards to contained views. Use is discouraged. +- (void)keepSize:(CGSize)size; + + + +#pragma mark Dimensions: Batch Convenience +/// Convenience methods applied to whole array, in the order they are in array. + +/// All contained views will share the same Width using given priority. Width of the first is tied to Width of the second and so on. +- (void)keepWidthsEqualWithPriority:(KeepPriority)priority; + +/// All contained views will share the same Width using Required priority. Use is discouraged. Width of the first is tied to Width of the second and so on. +- (void)keepWidthsEqual; + +/// All contained views will share the same Width using given priority. Height of the first is tied to Height of the second and so on. +- (void)keepHeightsEqualWithPriority:(KeepPriority)priority; + +/// All contained views will share the same Height using Required priority. Use is discouraged. Height of the first is tied to Height of the second and so on. +- (void)keepHeightsEqual; + +/// All contained views will share the same Size (Width + Height) using given priority. Width of the first is tied to Width of the second and so on. +- (void)keepSizesEqualWithPriority:(KeepPriority)priority; + +/// All contained views will share the same Size using Required priority. Use is discouraged. Size of the first is tied to Size of the second and so on. +- (void)keepSizesEqual; + + + + + +#pragma mark - +#pragma mark Superview Insets: Grouped + +/// Grouped attribute for Left Inset of contained views. +@property (readonly) KeepAttribute *keepLeftInset; + +/// Grouped attribute for Right Inset of contained views. +@property (readonly) KeepAttribute *keepRightInset; + +/// Grouped attribute for Leading Inset of contained views. +@property (readonly) KeepAttribute *keepLeadingInset; + +/// Grouped attribute for Trailing Inset of contained views. +@property (readonly) KeepAttribute *keepTrailingInset; + +/// Grouped attribute for Top Inset of contained views. +@property (readonly) KeepAttribute *keepTopInset; + +/// Grouped attribute for Bottom Inset of contained views. +@property (readonly) KeepAttribute *keepBottomInset; + +/// Grouped attribute for First Baseline Inset of contained views. +@property (nonatomic, readonly) KeepAttribute *keepFirstBaselineInset; + +/// Grouped attribute for Last Baseline Inset of contained views. +@property (nonatomic, readonly) KeepAttribute *keepLastBaselineInset; + +/// Grouped attribute for All Insets of contained views. +@property (readonly) KeepAttribute *keepInsets; + +/// Grouped attribute for Horizontal Insets of contained views. +@property (readonly) KeepAttribute *keepHorizontalInsets; + +/// Grouped attribute for Vertical Insets of contained views. +@property (readonly) KeepAttribute *keepVerticalInsets; + + + +#pragma mark Superview Insets: Forwarded + +/// Forwards to contained views. +- (void)keepInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority; + +/// Forwards to contained views. Use is discouraged. +- (void)keepInsets:(KPEdgeInsets)insets; + + + + + +#pragma mark - +#pragma mark Superview Safe Insets: Grouped + +/// Grouped attribute for Left Safe Inset of contained views. +@property (readonly) KeepAttribute *keepLeftSafeInset API_AVAILABLE(ios(11)); + +/// Grouped attribute for Right Safe Inset of contained views. +@property (readonly) KeepAttribute *keepRightSafeInset API_AVAILABLE(ios(11)); + +/// Grouped attribute for Leading Safe Inset of contained views. +@property (readonly) KeepAttribute *keepLeadingSafeInset API_AVAILABLE(ios(11)); + +/// Grouped attribute for Trailing Safe Inset of contained views. +@property (readonly) KeepAttribute *keepTrailingSafeInset API_AVAILABLE(ios(11)); + +/// Grouped attribute for Top Safe Inset of contained views. +@property (readonly) KeepAttribute *keepTopSafeInset API_AVAILABLE(ios(11)); + +/// Grouped attribute for Bottom Safe Inset of contained views. +@property (readonly) KeepAttribute *keepBottomSafeInset API_AVAILABLE(ios(11)); + +/// Grouped attribute for All Safe Insets of contained views. +@property (readonly) KeepAttribute *keepSafeInsets API_AVAILABLE(ios(11)); + +/// Grouped attribute for Horizontal Safe Insets of contained views. +@property (readonly) KeepAttribute *keepHorizontalSafeInsets API_AVAILABLE(ios(11)); + +/// Grouped attribute for Vertical Safe Insets of contained views. +@property (readonly) KeepAttribute *keepVerticalSafeInsets API_AVAILABLE(ios(11)); + + + +#pragma mark Superview Safe Insets: Forwarded + +/// Forwards to contained views. +- (void)keepSafeInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority API_AVAILABLE(ios(11)); + +/// Forwards to contained views. Use is discouraged. +- (void)keepSafeInsets:(KPEdgeInsets)insets API_AVAILABLE(ios(11)); + + + + + +#pragma mark - +#pragma mark Superview Margin Insets: Grouped + +/// Grouped attribute for Left Margin Inset of contained views. +@property (readonly) KeepAttribute *keepLeftMarginInset; + +/// Grouped attribute for Right Margin Inset of contained views. +@property (readonly) KeepAttribute *keepRightMarginInset; + +/// Grouped attribute for Leading Margin Inset of contained views. +@property (readonly) KeepAttribute *keepLeadingMarginInset; + +/// Grouped attribute for Trailing Margin Inset of contained views. +@property (readonly) KeepAttribute *keepTrailingMarginInset; + +/// Grouped attribute for Top Margin Inset of contained views. +@property (readonly) KeepAttribute *keepTopMarginInset; + +/// Grouped attribute for Bottom Margin Inset of contained views. +@property (readonly) KeepAttribute *keepBottomMarginInset; + +/// Grouped attribute for All Margin Insets of contained views. +@property (readonly) KeepAttribute *keepMarginInsets; + +/// Grouped attribute for Horizontal Margin Insets of contained views. +@property (readonly) KeepAttribute *keepHorizontalMarginInsets; + +/// Grouped attribute for Vertical Margin Insets of contained views. +@property (readonly) KeepAttribute *keepVerticalMarginInsets; + + + +#pragma mark Superview Margin Insets: Forwarded + +/// Forwards to contained views. +- (void)keepMarginInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority; + +/// Forwards to contained views. Use is discouraged. +- (void)keepMarginInsets:(KPEdgeInsets)insets; + + + + + +#pragma mark - +#pragma mark Center: Grouped + +/// Grouped attribute for Horizontal Center of contained views. +@property (readonly) KeepAttribute *keepHorizontalCenter; + +/// Grouped attribute for Vertical Center of contained views. +@property (readonly) KeepAttribute *keepVerticalCenter; + +/// Grouped attribute for Both Center Axis of contained views. +@property (readonly) KeepAttribute *keepCenter; + + + +#pragma mark Center: Forwarded + +/// Forwards to contained views. +- (void)keepCenteredWithPriority:(KeepPriority)priority; + +/// Forwards to contained views. Use is discouraged. +- (void)keepCentered; + +/// Forwards to contained views. +- (void)keepHorizontallyCenteredWithPriority:(KeepPriority)priority; + +/// Forwards to contained views. Use is discouraged. +- (void)keepHorizontallyCentered; + +/// Forwards to contained views. +- (void)keepVerticallyCenteredWithPriority:(KeepPriority)priority; + +/// Forwards to contained views. Use is discouraged. +- (void)keepVerticallyCentered; + +/// Forwards to contained views. +- (void)keepCenter:(CGPoint)center priority:(KeepPriority)priority; + +/// Forwards to contained views. Use is discouraged. +- (void)keepCenter:(CGPoint)center; + + + + + +#pragma mark - +#pragma mark Offsets: Grouped + +/// Grouped attribute for Left Offset of contained views. +@property (readonly) KeepRelatedAttributeBlock keepLeftOffsetTo; + +/// Grouped attribute for Right Offset of contained views. +@property (readonly) KeepRelatedAttributeBlock keepRightOffsetTo; + +/// Grouped attribute for Leading Offset of contained views. +@property (readonly) KeepRelatedAttributeBlock keepLeadingOffsetTo; + +/// Grouped attribute for Trailing Offset of contained views. +@property (readonly) KeepRelatedAttributeBlock keepTrailingOffsetTo; + +/// Grouped attribute for Top Offset of contained views. +@property (readonly) KeepRelatedAttributeBlock keepTopOffsetTo; + +/// Grouped attribute for Bottom Offset of contained views. +@property (readonly) KeepRelatedAttributeBlock keepBottomOffsetTo; + +/// Grouped attribute for First Baseline Offset of contained views. +@property (nonatomic, readonly) KeepRelatedAttributeBlock keepFirstBaselineOffsetTo; + +/// Grouped attribute for Last Baseline Offset of contained views. +@property (nonatomic, readonly) KeepRelatedAttributeBlock keepLastBaselineOffsetTo; + + + +#pragma mark Offsets: Batch Convenience +/// Convenience methods applied to whole array, in the order they are in array. + +/// All contained views will share the same Horizontal Offset (left to right) using given priority. First view will keep Right Offset to second view and so on. +- (void)keepHorizontalOffsets:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will share the same Leading Offset (depends on writing direction) using given priority. First view will keep Leading Offset to second view and so on. +- (void)keepLeadingOffsets:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will share the same Vertical Offset (top to bottom) using given priority. First view will keep Bottom Offset to second view and so on. +- (void)keepVerticalOffsets:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will share the same Baseline Offset (top to bottom) using given priority. First view will keep Last Baseline Offset to second view’ First Baseline and so on. +- (void)keepBaselineOffsets:(KeepValue)value KEEP_SWIFT_AWAY; + + + + + +#pragma mark - +#pragma mark Alignments: Grouped + +/// Grouped attribute for Left Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepLeftAlignTo; + +/// Grouped attribute for Right Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepRightAlignTo; + +/// Grouped attribute for Leading Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepLeadingAlignTo; + +/// Grouped attribute for Trailing Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepTrailingAlignTo; + +/// Grouped attribute for Top Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepTopAlignTo; + +/// Grouped attribute for Bottom Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepBottomAlignTo; + +/// Grouped attribute for All 4 Edge Alignments of contained views. +@property (readonly) KeepRelatedAttributeBlock keepEdgeAlignTo; + +/// Grouped attribute for Vertical Center Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepVerticalAlignTo; + +/// Grouped attribute for Horizontal Center Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepHorizontalAlignTo; + +/// Grouped attribute for Both Center Axis Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepCenterAlignTo; + +/// Grouped attribute for First Baseline Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepFirstBaselineAlignTo; + +/// Grouped attribute for Last Baseline Alignment of contained views. +@property (readonly) KeepRelatedAttributeBlock keepLastBaselineAlignTo; + + +#pragma mark Alignments: Batch Convenience +/// Convenience methods applied to whole array, in the order they are in array. + +/// All contained views will share the same Left Alignment. First view will keep Left Alignment (with offset) with second view and so on. +- (void)keepLeftAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will be aligned to the left. First view will keep Left Alignment with second view and so on. +- (void)keepLeftAligned; + +/// All contained views will share the same Right Alignment. First view will keep Right Alignment (with offset) with second view and so on. +- (void)keepRightAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will be aligned to the right. First view will keep Right Alignment with second view and so on. +- (void)keepRightAligned; + +/// All contained views will share the same Leading Alignment. First view will keep Leading Alignment (with offset) with second view and so on. +- (void)keepLeadingAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will be aligned to the leading side. First view will keep Leading Alignment with second view and so on. +- (void)keepLeadingAligned; + +/// All contained views will share the same Trailing Alignment. First view will keep Trailing Alignment (with offset) with second view and so on. +- (void)keepTrailingAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will be aligned to the trailing side. First view will keep Trailing Alignment with second view and so on. +- (void)keepTrailingAligned; + +/// All contained views will share the same Top Alignment. First view will keep Top Alignment (with offset) with second view and so on. +- (void)keepTopAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will be aligned to the top. First view will keep Top Alignment with second view and so on. +- (void)keepTopAligned; + +/// All contained views will share the same Bottom Alignment. First view will keep Bottom Alignment (with offset) with second view and so on. +- (void)keepBottomAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will be aligned to the bottom. First view will keep Bottom Alignment with second view and so on. +- (void)keepBottomAligned; + +/// All contained views will share the same Vertical Center Alignment. First view will keep Vertical Center Alignment (with offset) with second view and so on. +- (void)keepVerticalAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will vertically aligned. First view will keep Vertical Center Alignment with second view and so on. +- (void)keepVerticallyAligned; + +/// All contained views will share the same Horizontal Center Alignment. First view will keep Horizontal Center Alignment (with offset) with second view and so on. +- (void)keepHorizontalAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will horizontally aligned. First view will keep Horizontal Center Alignment with second view and so on. +- (void)keepHorizontallyAligned; + +/// All contained views will share the same First Baseline Alignment. First view will keep First Baseline Alignment (with offset) with second view and so on. +- (void)keepFirstBaselineAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will be aligned to their first baseline. First view will keep First Baseline Alignment with second view and so on. +- (void)keepFirstBaselineAligned; + +/// All contained views will share the same Last Baseline Alignment. First view will keep Last Baseline Alignment (with offset) with second view and so on. +- (void)keepLastBaselineAlignments:(KeepValue)value KEEP_SWIFT_AWAY; + +/// All contained views will aligned to their last baseline. First view will keep Last Baseline Alignment with second view and so on. +- (void)keepLastBaselineAligned; + + + + + +@end diff --git a/ChatSDKKeepLayout/Sources/KeepArray.m b/ChatSDKKeepLayout/Sources/KeepArray.m new file mode 100644 index 00000000..0170335e --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepArray.m @@ -0,0 +1,733 @@ +// +// KeepArray.m +// Keep Layout +// +// Created by Martin Kiss on 23.6.13. +// Copyright (c) 2013 Triceratops. All rights reserved. +// + +#import "KeepArray.h" +#import "KeepAttribute.h" +#import "KeepView.h" + + + + + +@implementation NSArray (KeepLayout) + + + + + +#pragma mark General + + +- (BOOL)keep_onlyContainsViews { + for (KPView *view in self) { + if ( ! [view isKindOfClass:[KPView class]]) { + return NO; + } + } + return YES; +} + + +- (KeepGroupAttribute *)keep_groupAttributeForSelector:(SEL)selector { + KeepAssert([self keep_onlyContainsViews], @"%@ can only be called on array of View objects", NSStringFromSelector(selector)); + + return [[KeepGroupAttribute alloc] initWithAttributes:[self valueForKeyPath:NSStringFromSelector(selector)]]; +} + + +- (KeepGroupAttribute *)keep_groupAttributeForSelector:(SEL)selector relatedView:(KPView *)relatedView { + KeepAssert([self keep_onlyContainsViews], @"%@ can only be called on array of View objects", NSStringFromSelector(selector)); + + NSMutableArray *builder = [[NSMutableArray alloc] initWithCapacity:self.count]; + for (KPView *view in self) { + KeepAttribute *(^block)(KPView *) = [view valueForKeyPath:NSStringFromSelector(selector)]; + [builder addObject:block(relatedView)]; + } + return [[KeepGroupAttribute alloc] initWithAttributes:builder]; +} + + +- (void)keep_invoke:(SEL)selector each:(void(^)(KPView *view))block { + KeepAssert([self keep_onlyContainsViews], @"%@ can only be called on array of View objects", NSStringFromSelector(selector)); + + for (KPView *view in self) { + block(view); + } +} + + +- (void)keep_invoke:(SEL)selector eachTwo:(void(^)(KPView *this, KPView *next))block { + KeepAssert([self keep_onlyContainsViews], @"%@ can only be called on array of View objects", NSStringFromSelector(selector)); + + if (self.count < 2) return; + + for (NSUInteger index = 0; index < self.count - 1; index++) { + KPView *this = [self objectAtIndex:index]; + KPView *next = [self objectAtIndex:index + 1]; + block(this, next); + } +} + + + + + +#pragma mark Dimensions + + +- (KeepAttribute *)keepWidth { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepHeight { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepSize { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (void)keepSize:(CGSize)size { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepSize:size]; + }]; +} + + +- (void)keepSize:(CGSize)size priority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepSize:size priority:priority]; + }]; +} + + +- (KeepAttribute *)keepAspectRatio { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepRelatedAttributeBlock)keepWidthTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepHeightTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepSizeTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (void)keepWidthsEqualWithPriority:(KeepPriority)priority { + [self keep_invoke:_cmd eachTwo:^(KPView *this, KPView *next) { + this.keepWidthTo(next).equal = KeepValueMake(1, priority); + }]; +} + + +- (void)keepHeightsEqualWithPriority:(KeepPriority)priority { + [self keep_invoke:_cmd eachTwo:^(KPView *this, KPView *next) { + this.keepHeightTo(next).equal = KeepValueMake(1, priority); + }]; +} + + +- (void)keepSizesEqualWithPriority:(KeepPriority)priority { + [self keep_invoke:_cmd eachTwo:^(KPView *this, KPView *next) { + this.keepSizeTo(next).equal = KeepValueMake(1, priority); + }]; +} + + +- (void)keepWidthsEqual { + [self keepWidthsEqualWithPriority:KeepPriorityRequired]; +} + + +- (void)keepHeightsEqual { + [self keepHeightsEqualWithPriority:KeepPriorityRequired]; +} + + +- (void)keepSizesEqual { + [self keepSizesEqualWithPriority:KeepPriorityRequired]; +} + + + + + +#pragma mark Superview Insets + + +- (KeepAttribute *)keepLeftInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepRightInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepLeadingInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepTrailingInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepTopInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepBottomInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepFirstBaselineInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepLastBaselineInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepHorizontalInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepVerticalInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (void)keepInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepInsets:insets priority:priority]; + }]; +} + + +- (void)keepInsets:(KPEdgeInsets)insets { + [self keepInsets:insets priority:KeepPriorityRequired]; +} + + + + + +#pragma mark Superview Safe Insets + + +- (KeepAttribute *)keepLeftSafeInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepRightSafeInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepLeadingSafeInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepTrailingSafeInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepTopSafeInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepBottomSafeInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepSafeInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepHorizontalSafeInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepVerticalSafeInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (void)keepSafeInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepSafeInsets:insets priority:priority]; + }]; +} + + +- (void)keepSafeInsets:(KPEdgeInsets)insets { + [self keepSafeInsets:insets priority:KeepPriorityRequired]; +} + + + + + +#pragma mark Superview Margin Insets + + +- (KeepAttribute *)keepLeftMarginInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepRightMarginInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepLeadingMarginInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepTrailingMarginInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepTopMarginInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepBottomMarginInset { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepMarginInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepHorizontalMarginInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepVerticalMarginInsets { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (void)keepMarginInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepMarginInsets:insets priority:priority]; + }]; +} + + +- (void)keepMarginInsets:(KPEdgeInsets)insets { + [self keepMarginInsets:insets priority:KeepPriorityRequired]; +} + + + + + +#pragma mark Center + +- (KeepAttribute *)keepHorizontalCenter { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepVerticalCenter { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (KeepAttribute *)keepCenter { + return [self keep_groupAttributeForSelector:_cmd]; +} + + +- (void)keepCenteredWithPriority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepCenteredWithPriority:priority]; + }]; +} + + +- (void)keepHorizontallyCenteredWithPriority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepHorizontallyCenteredWithPriority:priority]; + }]; +} + + +- (void)keepVerticallyCenteredWithPriority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepVerticallyCenteredWithPriority:priority]; + }]; +} + + +- (void)keepCenter:(CGPoint)center priority:(KeepPriority)priority { + [self keep_invoke:_cmd each:^(KPView *view) { + [view keepCenter:center priority:priority]; + }]; +} + + +- (void)keepCentered { + [self keepCenteredWithPriority:KeepPriorityRequired]; +} + + +- (void)keepHorizontallyCentered { + [self keepHorizontallyCenteredWithPriority:KeepPriorityRequired]; +} + + +- (void)keepVerticallyCentered { + [self keepVerticallyCenteredWithPriority:KeepPriorityRequired]; +} + + +- (void)keepCenter:(CGPoint)center { + [self keepCenter:center priority:KeepPriorityRequired]; +} + + + + + +#pragma mark Offsets + + +- (KeepRelatedAttributeBlock)keepLeftOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepRightOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepLeadingOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepTrailingOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepTopOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepBottomOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepFirstBaselineOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepLastBaselineOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (void)keepHorizontalOffsets:(KeepValue)value { + [self keep_invoke:_cmd eachTwo:^(KPView *this, KPView *next) { + this.keepRightOffsetTo(next).equal = value; + }]; +} + + +- (void)keepLeadingOffsets:(KeepValue)value { + [self keep_invoke:_cmd eachTwo:^(KPView *this, KPView *next) { + this.keepTrailingOffsetTo(next).equal = value; + }]; +} + + +- (void)keepVerticalOffsets:(KeepValue)value { + [self keep_invoke:_cmd eachTwo:^(KPView *this, KPView *next) { + this.keepBottomOffsetTo(next).equal = value; + }]; +} + + +- (void)keepBaselineOffsets:(KeepValue)value { + [self keep_invoke:_cmd eachTwo:^(KPView *this, KPView *next) { + this.keepLastBaselineOffsetTo(next).equal = value; + }]; +} + + + + + +#pragma mark Alignments + + +- (KeepRelatedAttributeBlock)keepLeftAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepRightAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepLeadingAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepTrailingAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepTopAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepBottomAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepEdgeAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepVerticalAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepHorizontalAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepCenterAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepFirstBaselineAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (KeepRelatedAttributeBlock)keepLastBaselineAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_groupAttributeForSelector:_cmd relatedView:view]; + }; +} + + +- (void)keep_alignedSelector:(SEL)selector invokeSelector:(SEL)invokeSelector value:(KeepValue)value { + [self keep_invoke:selector eachTwo:^(KPView *this, KPView *next) { + KeepAttribute *(^block)(KPView *) = [this valueForKey:NSStringFromSelector(invokeSelector)]; + KeepAttribute *attribute = block(next); + attribute.equal = value; + }]; +} + + +- (void)keepLeftAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepLeftAlignTo) value:value]; +} + + +- (void)keepRightAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepRightAlignTo) value:value]; +} + + +- (void)keepLeadingAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepLeadingAlignTo) value:value]; +} + + +- (void)keepTrailingAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepTrailingAlignTo) value:value]; +} + + +- (void)keepTopAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepTopAlignTo) value:value]; +} + + +- (void)keepBottomAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepBottomAlignTo) value:value]; +} + + +- (void)keepVerticalAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepVerticalAlignTo) value:value]; +} + + +- (void)keepHorizontalAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepHorizontalAlignTo) value:value]; +} + + +- (void)keepFirstBaselineAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepFirstBaselineAlignTo) value:value]; +} + + +- (void)keepLastBaselineAlignments:(KeepValue)value { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepLastBaselineAlignTo) value:value]; +} + + +- (void)keepLeftAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepLeftAlignTo) value:0]; +} + + +- (void)keepRightAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepRightAlignTo) value:0]; +} + + +- (void)keepLeadingAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepLeadingAlignTo) value:0]; +} + + +- (void)keepTrailingAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepTrailingAlignTo) value:0]; +} + + +- (void)keepTopAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepTopAlignTo) value:0]; +} + + +- (void)keepBottomAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepBottomAlignTo) value:0]; +} + + +- (void)keepVerticallyAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepVerticalAlignTo) value:0]; +} + + +- (void)keepHorizontallyAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepHorizontalAlignTo) value:0]; +} + + +- (void)keepFirstBaselineAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepFirstBaselineAlignTo) value:0]; +} + + +- (void)keepLastBaselineAligned { + [self keep_alignedSelector:_cmd invokeSelector:@selector(keepLastBaselineAlignTo) value:0]; +} + + + + + +@end diff --git a/ChatSDKKeepLayout/Sources/KeepAttribute.h b/ChatSDKKeepLayout/Sources/KeepAttribute.h new file mode 100644 index 00000000..e71ce2ac --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepAttribute.h @@ -0,0 +1,133 @@ +// +// KeepAttribute.h +// Keep Layout +// +// Created by Martin Kiss on 28.1.13. +// Copyright (c) 2013 Triceratops. All rights reserved. +// + +#import "KeepTypes.h" + + + +@class KeepAtomic; + + + + + +/// Each instance if KeepAttribute manages up to 3 NSLayoutConstraints: one for each relation. +/// Class cluster. +@interface KeepAttribute : NSObject + + + +#pragma mark Values +/// Value with priority to be applied to underlaying constraints. +@property KeepValue equal KEEP_SWIFT_AWAY NS_SWIFT_NAME(incompatible_equal); ///< Constraint with relation Equal +@property KeepValue max KEEP_SWIFT_AWAY NS_SWIFT_NAME(incompatible_max); ///< Constraint with relation GreaterThanOrEqual +@property KeepValue min KEEP_SWIFT_AWAY NS_SWIFT_NAME(incompatible_min); ///< Constraint with relation LessThanOrEqual + +- (void)keepAt:(KeepValue)equal min:(KeepValue)min KEEP_SWIFT_AWAY; +- (void)keepAt:(KeepValue)equal max:(KeepValue)max KEEP_SWIFT_AWAY; +- (void)keepAt:(KeepValue)equal min:(KeepValue)min max:(KeepValue)max KEEP_SWIFT_AWAY; +- (void)keepMin:(KeepValue)min max:(KeepValue)max KEEP_SWIFT_AWAY; + +- (BOOL)isRelatedToView:(KPView *)view; + +#pragma mark Swift Compatibility +/// Don’t use these directly. They are exposed for Swift extension to avoid KeepValue type. + +@property KeepValue_Decomposed decomposed_equal; +@property KeepValue_Decomposed decomposed_max; +@property KeepValue_Decomposed decomposed_min; + + +#pragma mark Activation +/// Whether at least one constraint is active. +@property (readonly) BOOL isActive; +/// Disables all managed constraints. +- (void)deactivate; + + +#pragma mark Grouping +/// Allows you to create groups of attributes. Grouped attribute forwards all methods to its children. ++ (KeepAttribute *)group:(KeepAttribute *)first, ... NS_REQUIRES_NIL_TERMINATION; + + +#pragma mark Debugging +/// Debugging helper. Name of attribute is a part of its `-description` +@property (copy) NSString *name; +- (instancetype)name:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + + + +@end + + + +@interface KeepAtomic : NSObject + +/// Executes block and returns group of all changed attributes. ++ (KeepAtomic *)layout:(void(^)(void))block; +/// Disables all managed constraints. +- (void)deactivate; + +@end + + + + + + + +/// Private protocol. +/// Used as common type for Views and Layout Guides. +@protocol KeepViewOrGuide @end +@interface KPView (KeepViewOrGuide) @end +@interface KPLayoutGuide (KeepViewOrGuide) @end + + + +/// Private class. +/// Used by Keep Layout implementation to create attributes. +@interface KeepSimpleAttribute : KeepAttribute + +/// Properties that don't change in time. +- (instancetype)initWithView:(KPView *)view + layoutAttribute:(NSLayoutAttribute)layoutAttribute + relatedView:(id)relatedViewOrGuide + relatedLayoutAttribute:(NSLayoutAttribute)relatedLayoutAttribute + coefficient:(CGFloat)coefficient; +/// Multiplier of values: equal, min and max +@property (readonly) CGFloat coefficient; + +@end + + + +/// Private class. +/// Used for attributes where the values are expressed as constants. +@interface KeepConstantAttribute : KeepSimpleAttribute +@end + + + +/// Private class. +/// Used for attributes where the values are expressed as multipliers. +@interface KeepMultiplierAttribute : KeepSimpleAttribute +@end + + + +/// Private class. +/// The `+group:` method returns instance of this class. Forwards methods from base class cluster interface to its children. +@interface KeepGroupAttribute : KeepAttribute + +- (instancetype)initWithAttributes:(id)attributes; +@property (readonly) id attributes; + +@end + + + diff --git a/ChatSDKKeepLayout/Sources/KeepAttribute.m b/ChatSDKKeepLayout/Sources/KeepAttribute.m new file mode 100644 index 00000000..d7702058 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepAttribute.m @@ -0,0 +1,804 @@ +// +// KeepAttribute.m +// Keep Layout +// +// Created by Martin Kiss on 28.1.13. +// Copyright (c) 2013 Triceratops. All rights reserved. +// + +#import "KeepAttribute.h" +#import "KeepView.h" +#import "KeepLayoutConstraint.h" + + + + + +@implementation KeepAttribute + + + + + +- (instancetype)init { + self = [super init]; + if (self) { + NSAssert(self.class != KeepAttribute.class, @"%@ is abstract class", self.class); + if (self.class == KeepAttribute.class) { + return nil; + } + } + return self; +} + + +- (BOOL)isRelatedToView:(UIView *)view { + return NO; +} + + + + + +#pragma mark Values + + +- (void)keepAt:(KeepValue)equal min:(KeepValue)min { + self.equal = KeepValueSetDefaultPriority(equal, KeepPriorityHigh); + self.min = min; +} + + +- (void)keepAt:(KeepValue)equal max:(KeepValue)max { + self.equal = KeepValueSetDefaultPriority(equal, KeepPriorityHigh); + self.max = max; +} + + +- (void)keepAt:(KeepValue)equal min:(KeepValue)min max:(KeepValue)max { + self.equal = KeepValueSetDefaultPriority(equal, KeepPriorityHigh); + self.min = min; + self.max = max; +} + + +- (void)keepMin:(KeepValue)min max:(KeepValue)max { + self.min = min; + self.max = max; +} + + + + + +#pragma mark Swift Compatibility + + +- (KeepValue_Decomposed)decomposed_equal { + KeepValue equal = self.equal; + return (KeepValue_Decomposed){ + .value = equal, + .priority = KeepValueGetPriority(equal), + }; +} + + +- (KeepValue_Decomposed)decomposed_min { + KeepValue min = self.min; + return (KeepValue_Decomposed){ + .value = min, + .priority = KeepValueGetPriority(min), + }; +} + + +- (KeepValue_Decomposed)decomposed_max { + KeepValue max = self.max; + return (KeepValue_Decomposed){ + .value = max, + .priority = KeepValueGetPriority(max), + }; +} + + +- (void)setDecomposed_equal:(KeepValue_Decomposed)decomposed { + self.equal = KeepValueMake(decomposed.value, decomposed.priority); +} + + +- (void)setDecomposed_min:(KeepValue_Decomposed)decomposed { + self.min = KeepValueMake(decomposed.value, decomposed.priority); +} + + +- (void)setDecomposed_max:(KeepValue_Decomposed)decomposed { + self.max = KeepValueMake(decomposed.value, decomposed.priority); +} + + + + + +#pragma mark Activation + + +- (BOOL)isActive { + NSAssert(NO, @"-[%@ %@] is abstract", KeepAttribute.class, NSStringFromSelector(_cmd)); + return NO; +} + + +- (void)deactivate { + NSAssert(NO, @"-[%@ %@] is abstract", KeepAttribute.class, NSStringFromSelector(_cmd)); +} + + + + + +#pragma mark Grouping + + ++ (KeepGroupAttribute *)group:(KeepAttribute *)first, ... NS_REQUIRES_NIL_TERMINATION { + va_list list; + va_start(list, first); + + NSMutableArray *attributes = [[NSMutableArray alloc] init]; + KeepAttribute *attribute = first; + while (attribute) { + [attributes addObject:attribute]; + attribute = va_arg(list, KeepAttribute *); + } + + va_end(list); + + return [[KeepGroupAttribute alloc] initWithAttributes:attributes]; +} + + + + + +#pragma mark Naming & Debugging + + +- (instancetype)name:(NSString *)format, ... { +#ifdef DEBUG + va_list arguments; + va_start(arguments, format); + + self.name = [[NSString alloc] initWithFormat:format arguments:arguments]; + + va_end(arguments); +#endif + return self; +} + + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p; %@ [%@ < %@ < %@]>", + self.class, + self, + self.name ?: @"(no name)", + KeepValueDescription(self.min), + KeepValueDescription(self.equal), + KeepValueDescription(self.max)]; +} + + + + + +@end + + + + + + + + + + +#pragma mark - + + +@interface KeepSimpleAttribute () + +@property (weak) KPView *view; +@property NSLayoutAttribute layoutAttribute; +@property (weak) KPView *relatedView; +@property (weak) KPLayoutGuide *relatedGuide; +@property NSLayoutAttribute relatedLayoutAttribute; + +@property CGFloat coefficient; + +@property KeepLayoutConstraint *equalConstraint; +@property KeepLayoutConstraint *maxConstraint; +@property KeepLayoutConstraint *minConstraint; + +- (KeepLayoutConstraint *)createConstraintWithRelation:(NSLayoutRelation)relation value:(KeepValue)value; +- (KeepLayoutConstraint *)applyValue:(KeepValue)value forConstraint:(KeepLayoutConstraint *)constraint relation:(NSLayoutRelation)relation; +- (void)setNameForConstraint:(KeepLayoutConstraint *)constraint relation:(NSLayoutRelation)relation value:(KeepValue)value; +- (void)activateConstraint:(KeepLayoutConstraint *)constraint active:(BOOL)active; + +@end + + + +@interface KeepAtomic () + ++ (instancetype)current; +- (void)addAttribute:(KeepAttribute *)attribute forRelation:(NSLayoutRelation)relation; +- (void)addConstraint:(KeepLayoutConstraint *)constraint active:(BOOL)active; + +@end + + + + + +@implementation KeepSimpleAttribute + + + + + +#pragma mark Initialization + + +- (instancetype)init { + return [self initWithView:nil + layoutAttribute:NSLayoutAttributeNotAnAttribute + relatedView:nil + relatedLayoutAttribute:NSLayoutAttributeNotAnAttribute + coefficient:0]; +} + + +- (instancetype)initWithView:(KPView *)view + layoutAttribute:(NSLayoutAttribute)layoutAttribute + relatedView:(id)relatedViewOrGuide + relatedLayoutAttribute:(NSLayoutAttribute)relatedLayoutAttribute + coefficient:(CGFloat)coefficient { + self = [super init]; + if (self) { + NSParameterAssert(view); + NSParameterAssert(layoutAttribute != NSLayoutAttributeNotAnAttribute); + NSParameterAssert(coefficient); + + NSAssert(self.class != KeepSimpleAttribute.class, @"%@ is abstract class", self.class); + if (self.class == KeepSimpleAttribute.class) { + return nil; + } + + self.view = view; + self.layoutAttribute = layoutAttribute; + + if ([relatedViewOrGuide isKindOfClass:KPView.class]) { + self.relatedView = (KPView*)relatedViewOrGuide; + } + if ([relatedViewOrGuide isKindOfClass:KPLayoutGuide.class]) { + self.relatedGuide = (KPLayoutGuide*)relatedViewOrGuide; + } + + self.relatedLayoutAttribute = relatedLayoutAttribute; + self.coefficient = coefficient; + } + return self; +} + + +- (BOOL)isRelatedToView:(UIView *)askedView { + if (askedView == nil) return NO; + + UIView *relatedView = self.relatedView; + UILayoutGuide *relatedGuide = self.relatedGuide; + + if (relatedView == askedView) { + return YES; /// Simple relation. + } + if (relatedGuide) { + return (relatedGuide.owningView == askedView); /// Related to guide that belongs to this view. + } + if (relatedView == nil) { + return (self.view == askedView); /// Related to owner view. + } + return NO; +} + + + + + +#pragma mark Constraints + + +- (KeepLayoutConstraint *)createConstraintWithRelation:(NSLayoutRelation)relation value:(KeepValue)value { + NSAssert(NO, @"-[%@ %@] is abstract", KeepSimpleAttribute.class, NSStringFromSelector(_cmd)); + return nil; +} + + +- (KeepLayoutConstraint *)applyValue:(KeepValue)value forConstraint:(KeepLayoutConstraint *)constraint relation:(NSLayoutRelation)relation { + NSAssert(NO, @"-[%@ %@] is abstract", KeepSimpleAttribute.class, NSStringFromSelector(_cmd)); + return nil; +} + + +- (BOOL)isActive { + return (self.equalConstraint.isActive || self.maxConstraint.isActive || self.minConstraint.isActive); +} + + +- (void)activateConstraint:(KeepLayoutConstraint *)constraint active:(BOOL)active { + if (constraint.active != active) { + KeepAtomic *atomic = [KeepAtomic current]; + if (atomic) { + [atomic addConstraint:constraint active:active]; + } + else { + [constraint setActive:active]; + } + } +} + + +- (void)deactivate { + self.equal = KeepNone; + self.max = KeepNone; + self.min = KeepNone; +} + + + + + +#pragma mark Values + + +- (KeepLayoutConstraint *)adjustConstraint:(KeepLayoutConstraint *)constraint forRelation:(NSLayoutRelation)relation value:(KeepValue)value { + BOOL isNone = KeepValueIsNone(value); + if (isNone) { + [self activateConstraint:constraint active:NO]; + constraint = nil; + } + else { + value = KeepValueSetDefaultPriority(value, KeepPriorityRequired); + if ( ! constraint) { + constraint = [self createConstraintWithRelation:relation value:value]; + } + else { + constraint = [self applyValue:value forConstraint:constraint relation:relation]; + } + [self activateConstraint:constraint active:YES]; + [self setNameForConstraint:constraint relation:relation value:value]; + [[KeepAtomic current] addAttribute:self forRelation:relation]; + } + return constraint; +} + + +- (void)setEqual:(KeepValue)equal { + KeepValue adjustedEqual = KeepValueSetDefaultPriority(equal, KeepPriorityRequired); + [super setEqual:adjustedEqual]; + self.equalConstraint = [self adjustConstraint:self.equalConstraint forRelation:NSLayoutRelationEqual value:equal]; +} + + +- (void)setMax:(KeepValue)max { + KeepValue adjustedMax = KeepValueSetDefaultPriority(max, KeepPriorityRequired); + [super setMax:adjustedMax]; + self.maxConstraint = [self adjustConstraint:self.maxConstraint forRelation:NSLayoutRelationLessThanOrEqual value:max]; +} + + +- (void)setMin:(KeepValue)min { + KeepValue adjustedMin = KeepValueSetDefaultPriority(min, KeepPriorityRequired); + [super setMin:adjustedMin]; + self.minConstraint = [self adjustConstraint:self.minConstraint forRelation:NSLayoutRelationGreaterThanOrEqual value:min]; +} + + + +- (void)setNameForConstraint:(KeepLayoutConstraint *)constraint relation:(NSLayoutRelation)relation value:(KeepValue)value { +#ifdef DEBUG + NSDictionary *relationNames = @{ + @(NSLayoutRelationEqual) : @"equal to", + @(NSLayoutRelationGreaterThanOrEqual) : @"at least", + @(NSLayoutRelationLessThanOrEqual) : @"at most", + }; + [constraint name:@"%@ %@ %@ with %@ priority", self.name, [relationNames objectForKey:@(relation)], @((double)value), KeepPriorityDescription(KeepValueGetPriority(value))]; +#endif +} + + + + + +@end + + + + + + + + + + +#pragma mark - + + +@implementation KeepConstantAttribute + + + + + +#pragma mark Constraint Overrides + + +- (KeepLayoutConstraint *)createConstraintWithRelation:(NSLayoutRelation)relation value:(KeepValue)value { + if (self.coefficient < 0) { + if (relation == NSLayoutRelationGreaterThanOrEqual) relation = NSLayoutRelationLessThanOrEqual; + else if (relation == NSLayoutRelationLessThanOrEqual) relation = NSLayoutRelationGreaterThanOrEqual; + } + KeepLayoutConstraint *constraint = [KeepLayoutConstraint constraintWithItem:self.view attribute:self.layoutAttribute + relatedBy:relation + toItem:self.relatedView ?: self.relatedGuide attribute:self.relatedLayoutAttribute + multiplier:1 constant:value * self.coefficient]; + constraint.priority = KeepValueGetPriority(value); + return constraint; +} + + +- (KeepLayoutConstraint *)applyValue:(KeepValue)value forConstraint:(KeepLayoutConstraint *)constraint relation:(NSLayoutRelation)relation { + BOOL wasRequired = (constraint.priority == KeepPriorityRequired); + BOOL isRequired = (KeepValueGetPriority(value) == KeepPriorityRequired); + if (isRequired != wasRequired) { + /// “Priorities may not change from non-required to required or visa versa.” + [self activateConstraint:constraint active:NO]; + constraint = [self createConstraintWithRelation:relation value:value]; + [self activateConstraint:constraint active:YES]; + + } + else if ( ! isRequired) { + constraint.priority = KeepValueGetPriority(value); + } + constraint.constant = value * self.coefficient; + + return constraint; +} + + + + + +@end + + + + + + + + + + +#pragma mark - + + + +@implementation KeepMultiplierAttribute + + + + + +#pragma mark Constraint Overrides + + +- (KeepLayoutConstraint *)createConstraintWithRelation:(NSLayoutRelation)relation value:(KeepValue)value { + KeepLayoutConstraint *constraint = [KeepLayoutConstraint constraintWithItem:self.view attribute:self.layoutAttribute + relatedBy:relation + toItem:self.relatedView ?: self.relatedGuide attribute:self.relatedLayoutAttribute + multiplier:value * self.coefficient constant:0]; + constraint.priority = KeepValueGetPriority(value); + return constraint; +} + + +- (KeepLayoutConstraint *)applyValue:(KeepValue)value forConstraint:(KeepLayoutConstraint *)constraint relation:(NSLayoutRelation)relation { + [self activateConstraint:constraint active:NO]; + constraint = [self createConstraintWithRelation:relation value:value]; + [self activateConstraint:constraint active:YES]; + return constraint; +} + + + + + +@end + + + + + + + + + +#pragma mark - + + +@interface KeepGroupAttribute () + + +@property id attributes; + + +@end + + + + + +@implementation KeepGroupAttribute + + + + + +#pragma mark Initialization + + +- (id)init { + return [self initWithAttributes:nil]; +} + + +- (instancetype)initWithAttributes:(id)attributes { + self = [super init]; + if (self) { + NSParameterAssert(attributes); + + self.attributes = attributes; + } + return self; +} + + +- (BOOL)isRelatedToView:(UIView *)view { + for (KeepAttribute *attribute in self.attributes) { + if ( ! [attribute isRelatedToView:view]) { + return NO; + } + } + return YES; +} + + + + + + +#pragma mark Debugging + + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p; %@ %@>", self.class, self, self.name ?: @"(no name)", [self valueForKeyPath:@"attributes.description"]]; +} + + + + + +#pragma mark Accessing Values + +- (KeepValue)equal { + NSLog(@"Warning! Accessing property %@ for grouped attribute, returning KeepNone.", NSStringFromSelector(_cmd)); + return KeepNone; +} + +- (KeepValue)min { + NSLog(@"Warning! Accessing property %@ for grouped attribute, returning KeepNone.", NSStringFromSelector(_cmd)); + return KeepNone; +} + +- (KeepValue)max { + NSLog(@"Warning! Accessing property %@ for grouped attribute, returning KeepNone.", NSStringFromSelector(_cmd)); + return KeepNone; +} + + + + + +#pragma mark Setting Values + + +- (void)setEqual:(KeepValue)equal { + for (KeepAttribute *attribute in self.attributes) attribute.equal = equal; +} + + +- (void)setMax:(KeepValue)max { + for (KeepAttribute *attribute in self.attributes) attribute.max = max; +} + + +- (void)setMin:(KeepValue)min { + for (KeepAttribute *attribute in self.attributes) attribute.min = min; +} + + + + + +#pragma mark Activation + + +- (BOOL)isActive { + for (KeepAttribute *attribute in self.attributes) { + if (attribute.isActive) { + return YES; + } + } + return NO; +} + + +- (void)deactivate { + for (KeepAttribute *attribute in self.attributes) { + [attribute deactivate]; + } +} + + + + + +@end + + + + + + + + + +#pragma mark - + + +@interface KeepAtomic () + +@property (readonly) NSMutableSet *equalAttributes; +@property (readonly) NSMutableSet *minAttributes; +@property (readonly) NSMutableSet *maxAttributes; + +@property (readonly) NSMutableArray *activeConstraints; +@property (readonly) NSMutableArray *inactiveConstraints; + +@end + + + + + +@implementation KeepAtomic + + + + + +#pragma mark Initialization + + +- (instancetype)init { + self = [super init]; + if (self) { + self->_equalAttributes = [[NSMutableSet alloc] init]; + self->_minAttributes = [[NSMutableSet alloc] init]; + self->_maxAttributes = [[NSMutableSet alloc] init]; + + self->_activeConstraints = [NSMutableArray new]; + self->_inactiveConstraints = [NSMutableArray new]; + + } + return self; +} + + ++ (KeepAtomic *)layout:(void (^)(void))block { + KeepAtomic *atomic = [KeepAtomic new]; + [KeepAtomic setCurrent:atomic]; + block(); + [KeepAtomic setCurrent:nil]; + [NSLayoutConstraint deactivateConstraints:atomic.inactiveConstraints]; + [NSLayoutConstraint activateConstraints:atomic.activeConstraints]; + [atomic.activeConstraints removeAllObjects]; + [atomic.inactiveConstraints removeAllObjects]; + return atomic; +} + + + + + +#pragma mark Building + + +static NSMutableArray *KeepAtomicStack = nil; + + ++ (KeepAtomic *)current { + return KeepAtomicStack.lastObject; +} + + ++ (void)setCurrent:(KeepAtomic *)current { + if ( ! KeepAtomicStack) { + KeepAtomicStack = [NSMutableArray array]; + } + if (current) { + [KeepAtomicStack addObject:current]; + } + else { + [KeepAtomicStack removeLastObject]; + } +} + + +- (void)addAttribute:(KeepAttribute *)attribute forRelation:(NSLayoutRelation)relation { + switch (relation) { + case NSLayoutRelationEqual: [self.equalAttributes addObject:attribute]; break; + case NSLayoutRelationLessThanOrEqual: [self.maxAttributes addObject:attribute]; break; + case NSLayoutRelationGreaterThanOrEqual: [self.minAttributes addObject:attribute]; break; + } +} + + +- (void)addConstraint:(KeepLayoutConstraint *)constraint active:(BOOL)active { + if (active) { + [self.activeConstraints addObject:constraint]; + } + else { + [self.inactiveConstraints addObject:constraint]; + } +} + + + + + +#pragma mark Activation + + +- (void)deactivate { + [KeepAtomic layout:^{ + for (KeepAttribute *attribute in self.equalAttributes) attribute.equal = KeepNone; + for (KeepAttribute *attribute in self.minAttributes) attribute.min = KeepNone; + for (KeepAttribute *attribute in self.maxAttributes) attribute.max = KeepNone; + }]; + [self.equalAttributes removeAllObjects]; + [self.minAttributes removeAllObjects]; + [self.maxAttributes removeAllObjects]; +} + + +- (void)remove { + [self deactivate]; +} + + + + + +@end + + + + diff --git a/ChatSDKKeepLayout/Sources/KeepLayout.h b/ChatSDKKeepLayout/Sources/KeepLayout.h new file mode 100644 index 00000000..abed3e32 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepLayout.h @@ -0,0 +1,27 @@ +// +// KeepLayout.h +// Keep Layout +// +// Created by Martin Kiss on 28.1.13. +// Copyright (c) 2013 Triceratops. All rights reserved. +// + +#import + + + +FOUNDATION_EXPORT double KeepLayoutVersionNumber; +FOUNDATION_EXPORT const unsigned char KeepLayoutVersionString[]; + + + +#import "KeepTypes.h" +#import "KeepAttribute.h" +#import "KeepView.h" +#import "KeepArray.h" +#import "KeepLayoutConstraint.h" + +#if TARGET_OS_IOS +#import "UIViewController+KeepLayout.h" +#import "UIScrollView+KeepLayout.h" +#endif diff --git a/ChatSDKKeepLayout/Sources/KeepLayout.swift b/ChatSDKKeepLayout/Sources/KeepLayout.swift new file mode 100644 index 00000000..458c0478 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepLayout.swift @@ -0,0 +1,154 @@ +// +// KeepLayout.swift +// Keep Layout +// +// Created by Martin Kiss on 24.4.16. +// Copyright © 2016 Martin Kiss. All rights reserved. +// + + + +//MARK: KeepAttribute + Swift +/// Extension to redefine properties using Swift-compatible types. +protocol KeepAttribute_SwiftCompatibility { + var equal: KeepValue { get set } + var max: KeepValue { get set } + var min: KeepValue { get set } +} + + + +//MARK: KeepValue + Swift +/// Redefined type to be usable from Swift. +public protocol KeepValue { + var value: CGFloat { get } + var priority: KeepPriority { get } +} + +/// Default implementation for KeepValue.priority +public extension KeepValue { + var priority: KeepPriority { + return KeepPriorityRequired + } + var isNone: Bool { + return self.value.isNaN + } +} + +/// Value, that represents no value. KeepNone.isNone will return true. +public let KeepNone: KeepValue = KeepValue_Decomposed(value: CGFloat.nan, priority: 0) + +/// Constructor with arbitrary priority. +public func KeepValueMake(_ value: KeepValue, _ priority: KeepPriority) -> KeepValue { + return KeepValue_Decomposed(value: value.value, priority: priority) +} + +/// Constructors for 4 basic priorities +public func KeepRequired(_ value: KeepValue) -> KeepValue { + return KeepValueMake(value, KeepPriorityRequired) +} +public func KeepHigh(_ value: KeepValue) -> KeepValue { + return KeepValueMake(value, KeepPriorityHigh) +} +public func KeepLow(_ value: KeepValue) -> KeepValue { + return KeepValueMake(value, KeepPriorityLow) +} +public func KeepFitting(_ value: KeepValue) -> KeepValue { + return KeepValueMake(value, KeepPriorityFitting) +} + + + +//MARK: KeepValue + Numbers + +extension CGFloat: KeepValue { + public var value: CGFloat { + return self + } +} + +extension Double: KeepValue { + public var value: CGFloat { + return CGFloat(self) + } +} + +extension Float: KeepValue { + public var value: CGFloat { + return CGFloat(self) + } +} + +extension Int: KeepValue { + public var value: CGFloat { + return CGFloat(self) + } +} + +extension UInt: KeepValue { + public var value: CGFloat { + return CGFloat(self) + } +} + + + + + +//MARK: - Private Implementation + + + +/// Easy comversion to bridging KeepValue type. +private extension KeepValue { + var decomposed: KeepValue_Decomposed { + return KeepValue_Decomposed(value: self.value, priority: self.priority) + } +} + + + +/// Internal conformance to KeepValue protocol. +extension KeepValue_Decomposed: KeepValue { + var decomposed: KeepValue_Decomposed { + return self + } +} + + + +/// Implementation of compatibility accessors. +extension KeepAttribute: KeepAttribute_SwiftCompatibility { + + private class var ThereIsABugInSwiftWhereRemovingThisPropertyCausesCompilationError: Bool { return true } + + public var equal: KeepValue { + get { + return self.decomposed_equal + } + set { + self.decomposed_equal = newValue.decomposed + } + } + + public var max: KeepValue { + get { + return self.decomposed_max + } + set { + self.decomposed_max = newValue.decomposed + } + } + + public var min: KeepValue { + get { + return self.decomposed_min + } + set { + self.decomposed_min = newValue.decomposed + } + } + +} + + diff --git a/ChatSDKKeepLayout/Sources/KeepLayoutConstraint.h b/ChatSDKKeepLayout/Sources/KeepLayoutConstraint.h new file mode 100644 index 00000000..44d33072 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepLayoutConstraint.h @@ -0,0 +1,26 @@ + +// KeepLayoutConstraint.h +// Keep Layout +// +// Created by Martin Kiss on 18.7.13. +// Copyright (c) 2013 Martin Kiss. All rights reserved. +// + +#import "KeepTypes.h" + + + +@interface KeepLayoutConstraint : NSLayoutConstraint + + +#pragma mark Debugging +/// Debugging helper. Name of the constraint is a part of its `-description` +@property (copy) NSString *name; +- (instancetype)name:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + +- (NSString *)description; + + +@end + + diff --git a/ChatSDKKeepLayout/Sources/KeepLayoutConstraint.m b/ChatSDKKeepLayout/Sources/KeepLayoutConstraint.m new file mode 100644 index 00000000..41865966 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepLayoutConstraint.m @@ -0,0 +1,42 @@ +// +// KeepLayoutConstraint.m +// Keep Layout +// +// Created by Martin Kiss on 18.7.13. +// Copyright (c) 2013 Martin Kiss. All rights reserved. +// + +#import "KeepLayoutConstraint.h" +#import "KeepView.h" + + + + + +@implementation KeepLayoutConstraint + + +- (instancetype)name:(NSString *)format, ... { +#ifdef DEBUG + va_list arguments; + va_start(arguments, format); + + self.name = [[NSString alloc] initWithFormat:format arguments:arguments]; + + va_end(arguments); +#endif + return self; +} + +- (NSString *)description { + if (self.name) { + return [NSString stringWithFormat: @"<%@:%p %@>", self.class, self, self.name]; + } + else { + return [super description]; + } +} + + +@end + diff --git a/ChatSDKKeepLayout/Sources/KeepTypes.h b/ChatSDKKeepLayout/Sources/KeepTypes.h new file mode 100644 index 00000000..2f004756 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepTypes.h @@ -0,0 +1,123 @@ +// +// KeepTypes.h +// Keep Layout +// +// Created by Martin Kiss on 28.1.13. +// Copyright (c) 2013 Triceratops. All rights reserved. +// + +#import +#import + + + +#define KeepAssert(CONDITION, DESCRIPTION...) NSCAssert((CONDITION), @"Keep Layout: " DESCRIPTION) +#define KeepParameterAssert(CONDITION) NSCAssert((CONDITION), @"Keep Layout: " @"Invalid parameter not satisfying: %s", #CONDITION) + +#define KEEP_SWIFT_AWAY \ + NS_SWIFT_UNAVAILABLE("Not available in Swift.") + + + +#if TARGET_OS_IOS + +#import +#define KPView UIView +#define KPLayoutGuide UILayoutGuide +#define KPEdgeInsets UIEdgeInsets +#define KPEdgeInsetsZero UIEdgeInsetsZero + +#define KPOffset UIOffset +#define KPOffsetZero UIOffsetZero + +/// Use custom names. +typedef float KeepPriority; +static const KeepPriority KeepPriorityRequired = UILayoutPriorityRequired; +static const KeepPriority KeepPriorityHigh = UILayoutPriorityDefaultHigh; +static const KeepPriority KeepPriorityLow = UILayoutPriorityDefaultLow; +static const KeepPriority KeepPriorityFitting = UILayoutPriorityFittingSizeLevel; + +#else + +#import +#define KPView NSView +#define KPLayoutGuide NSLayoutGuide +#define KPEdgeInsets NSEdgeInsets + +extern const KPEdgeInsets KPEdgeInsetsZero; + +typedef struct KPOffset { + CGFloat horizontal, vertical; +} KPOffset; + +static inline KPOffset KPOffsetMake(CGFloat horizontal, CGFloat vertical) { + KPOffset offset = {horizontal, vertical}; + return offset; +} + +extern const KPOffset KPOffsetZero; + +/// Use custom names. +typedef float KeepPriority; +static const KeepPriority KeepPriorityRequired = NSLayoutPriorityRequired; +static const KeepPriority KeepPriorityHigh = NSLayoutPriorityDefaultHigh; +static const KeepPriority KeepPriorityLow = NSLayoutPriorityDefaultLow; +static const KeepPriority KeepPriorityFitting = NSLayoutPriorityFittingSizeCompression; + +/// OS X doesn’t have margins. +#define NSLayoutAttributeTopMargin NSLayoutAttributeTop +#define NSLayoutAttributeLeftMargin NSLayoutAttributeLeft +#define NSLayoutAttributeRightMargin NSLayoutAttributeRight +#define NSLayoutAttributeBottomMargin NSLayoutAttributeBottom +#define NSLayoutAttributeLeadingMargin NSLayoutAttributeLeading +#define NSLayoutAttributeTrailingMargin NSLayoutAttributeTrailing + +#endif + + +extern NSString *KeepPriorityDescription(KeepPriority); + + + +#pragma mark Value +/// Represents a value with associated priority (imaginary part). Used as values for attributes and underlaying constraints. Complex type is used to provide compatibility with scalars. +typedef _Complex double KeepValue KEEP_SWIFT_AWAY; + +/// Extracts priority (imaginary part). The value itself can be obtained by casting to double. +extern double KeepValueGetPriority(KeepValue) KEEP_SWIFT_AWAY; +/// If the priority is 0, sets the priority provided. +extern KeepValue KeepValueSetDefaultPriority(KeepValue, KeepPriority) KEEP_SWIFT_AWAY; + +/// Use these macros to build KeepValues easily: x = 10 keepHigh; +#define keepAt(Priority) +((Priority) * 1i) +#define keepRequired keepAt(KeepPriorityRequired) +#define keepHigh keepAt(KeepPriorityHigh) +#define keepLow keepAt(KeepPriorityLow) +#define keepFitting keepAt(KeepPriorityFitting) + +/// Value, that represents no value. KeepValueIsNone will return YES. +extern const KeepValue KeepNone KEEP_SWIFT_AWAY; +/// Returns YES for any value that has real value of NAN. +extern BOOL KeepValueIsNone(KeepValue) KEEP_SWIFT_AWAY; + +/// Constructor with arbitrary priority +extern KeepValue KeepValueMake(CGFloat, KeepPriority) KEEP_SWIFT_AWAY; +/// Constructors for 4 basic priorities +extern KeepValue KeepRequired(CGFloat) KEEP_SWIFT_AWAY; +extern KeepValue KeepHigh(CGFloat) KEEP_SWIFT_AWAY; +extern KeepValue KeepLow(CGFloat) KEEP_SWIFT_AWAY; +extern KeepValue KeepFitting(CGFloat) KEEP_SWIFT_AWAY; + +/// Debug description (example “42@750”, or just “42” if priority is Required) +extern NSString *KeepValueDescription(KeepValue) KEEP_SWIFT_AWAY; + + + +#pragma mark Swift Compatibility + +typedef struct { + CGFloat value; + KeepPriority priority; +} KeepValue_Decomposed; + + diff --git a/ChatSDKKeepLayout/Sources/KeepTypes.m b/ChatSDKKeepLayout/Sources/KeepTypes.m new file mode 100644 index 00000000..315a75a1 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepTypes.m @@ -0,0 +1,131 @@ +// +// KeepTypes.m +// Keep Layout +// +// Created by Martin Kiss on 19.6.13. +// Copyright (c) 2013 Triceratops. All rights reserved. +// + +#import "KeepTypes.h" +#import + + + +#if TARGET_OS_IOS + + +#else + +const KPEdgeInsets KPEdgeInsetsZero = (KPEdgeInsets){.top = 0, .left = 0, .bottom = 0, .right = 0}; +const KPOffset KPOffsetZero = (KPOffset){.horizontal = 0, .vertical = 0}; + +#endif + + + +extern NSString *KeepPriorityDescription(KeepPriority priority) { + NSString *name = @""; + + if (priority > KeepPriorityRequired || isnan(priority) || priority <= 0) { + name = @"undefined"; + } + else if (priority >= (KeepPriorityRequired + KeepPriorityHigh) / 2) { + priority -= KeepPriorityRequired; + name = @"required"; + } + else if (priority >= (KeepPriorityHigh + KeepPriorityLow) / 2) { + priority -= KeepPriorityHigh; + name = @"high"; + } + else if (priority >= (KeepPriorityLow + KeepPriorityFitting) / 2) { + priority -= KeepPriorityLow; + name = @"low"; + } + else { + priority -= KeepPriorityFitting; + name = @"fitting"; + } + + if (priority) { + name = [name stringByAppendingFormat:@"(%+g)", priority]; + } + + return name; +} + + + + + +double KeepValueGetPriority(KeepValue value) { + return cimag(value); +} + + +KeepValue KeepValueSetDefaultPriority(KeepValue value, KeepPriority priority) { + if (KeepValueIsNone(value)) return KeepNone; + + if (KeepValueGetPriority(value) == 0) { + return KeepValueMake(value, priority); + } + else { + return value; + } +} + + + + + +const KeepValue KeepNone = { NAN, 0 }; + + +BOOL KeepValueIsNone(KeepValue keepValue) { + double value = keepValue; + return isnan(value); +} + + + + + +KeepValue KeepValueMake(CGFloat value, KeepPriority priority) { + return (KeepValue) { value, priority }; +} + + +KeepValue KeepRequired(CGFloat value) { + return KeepValueMake(value, KeepPriorityRequired); +} + + +KeepValue KeepHigh(CGFloat value) { + return KeepValueMake(value, KeepPriorityHigh); +} + + +KeepValue KeepLow(CGFloat value) { + return KeepValueMake(value, KeepPriorityLow); +} + + +KeepValue KeepFitting(CGFloat value) { + return KeepValueMake(value, KeepPriorityFitting); +} + + + + + +NSString *KeepValueDescription(KeepValue value) { + if (KeepValueIsNone(value)) return @"none"; + + NSString *description = @((double)value).stringValue; + KeepPriority priority = KeepValueGetPriority(value); + if (priority != KeepPriorityRequired) { + description = [description stringByAppendingFormat:@"@%@", @(priority).stringValue]; + } + return description; +} + + diff --git a/ChatSDKKeepLayout/Sources/KeepView.h b/ChatSDKKeepLayout/Sources/KeepView.h new file mode 100644 index 00000000..3c536e87 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepView.h @@ -0,0 +1,403 @@ +// +// KeepView.h +// Keep Layout +// +// Created by Martin Kiss on 21.10.12. +// Copyright (c) 2012 iMartin Kiss. All rights reserved. +// + +#import "KeepTypes.h" + +@class KeepAttribute; + + +typedef KeepAttribute *(^KeepRelatedAttributeBlock)(KPView *otherView); + + + +/** + Category that provides very easy access to all Auto Layout features through abstraction on top of NSLayoutConstraints. + + Usage of methods returning KeepAttribute: + \code + view.keepWidth.equal = 320; + \endcode + + Usage of methods returning KeepRelatedAttributeBlock: + \code + view.keepWidthTo(anotherView).equal = 2; // 2x wider + \endcode + + **/ +@interface KPView (KeepLayout) + + + + + +#pragma mark - +#pragma mark Dimensions: Core +/// Attributes representing internal size of the receiver. + +/// Width of the receiver. +@property (readonly) KeepAttribute *keepWidth; + +/// Height of the receiver. +@property (readonly) KeepAttribute *keepHeight; + +/// Aspect Ratio of receiver's dimensions in the order Width/Height. For example 16/9 or 4/3. +@property (readonly) KeepAttribute *keepAspectRatio; + +/// Relative Width to other view. Value is multiplier of the other view's dimension. Both views must be in the same hierarchy. +@property (readonly) KeepRelatedAttributeBlock keepWidthTo; + +/// Relative Height to other view. Value is multiplier of the other view's dimension. Both views must be in the same hierarchy. +@property (readonly) KeepRelatedAttributeBlock keepHeightTo; + + + +#pragma mark Dimensions: Convenience +/// Convenience methods for setting both dimensions at once. + +/// Grouped proxy attribute for Width and Height. +@property (readonly) KeepAttribute *keepSize; + +/// Grouped proxy attribute for Relative Width and Height. Values are multipliers of the other view's dimensions. Both views must be in the same hierarchy. +@property (readonly) KeepRelatedAttributeBlock keepSizeTo; + +/// Sets custom Width and Height at once with given priority. +- (void)keepSize:(CGSize)size priority:(KeepPriority)priority; + +/// Sets custom Width and Height at once with Required priority. Use is discouraged. +- (void)keepSize:(CGSize)size; + + + + + +#pragma mark - +#pragma mark Superview Insets: Core +/// Attributes representing internal inset of the receiver to its current superview. + +/// Left Inset of the receiver in its current superview. +@property (readonly) KeepAttribute *keepLeftInset; + +/// Right Inset of the receiver in its current superview. Values are inverted to Right-to-Left direction. +@property (readonly) KeepAttribute *keepRightInset; + +/// Leading Inset of the receiver in its current superview. Depends on writing direction. +@property (readonly) KeepAttribute *keepLeadingInset; + +/// Trailing Inset of the receiver in its current superview. Values are inverted to Right-to-Left direction. Depends on writing direction. +@property (readonly) KeepAttribute *keepTrailingInset; + +/// Top Inset of the receiver in its current superview. +@property (readonly) KeepAttribute *keepTopInset; + +/// Bottom Inset of the receiver in its current superview. Values are inverted to Bottom-to-Top direction. +@property (readonly) KeepAttribute *keepBottomInset; + +/// First Baseline Inset of the receiver in its current superview from Top Edge. Not all views have baselines. +@property (nonatomic, readonly) KeepAttribute *keepFirstBaselineInset; + +/// Last Baseline Inset of the receiver in its current superview from Bottom Edge. Not all views have baselines. Values are inverted to Bottom-to-Top direction. +@property (nonatomic, readonly) KeepAttribute *keepLastBaselineInset; + + + +#pragma mark Superview Insets: Convenience +/// Convenience methods for setting all insets at once. + +/// Grouped proxy attribute for Top, Bottom, Left and Right Inset. +@property (readonly) KeepAttribute *keepInsets; + +/// Grouped proxy attribute for Left and Right Inset. +@property (readonly) KeepAttribute *keepHorizontalInsets; + +/// Grouped proxy attribute for Top and Bottom Inset. +@property (readonly) KeepAttribute *keepVerticalInsets; + +/// Sets custom Insets using given priority. +- (void)keepInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority; + +/// Sets custom Insets using Required priority. Use is discouraged. +- (void)keepInsets:(KPEdgeInsets)insets; + + + + + +#pragma mark - +#pragma mark Superview Safe Insets: Core +/// Attributes representing internal inset of the receive to its current superview, taking into account its safe insets. + +/// Left Safe Inset of the receiver in its current superview. +@property (readonly) KeepAttribute *keepLeftSafeInset API_AVAILABLE(ios(11)); + +/// Right Safe Inset of the receiver in its current superview. Values are inverted to Right-to-Left direction. +@property (readonly) KeepAttribute *keepRightSafeInset API_AVAILABLE(ios(11)); + +/// Leading Safe Inset of the receiver in its current superview. Depends on writing direction. +@property (readonly) KeepAttribute *keepLeadingSafeInset API_AVAILABLE(ios(11)); + +/// Trailing Safe Inset of the receiver in its current superview. Values are inverted to Right-to-Left direction. Depends on writing direction. +@property (readonly) KeepAttribute *keepTrailingSafeInset API_AVAILABLE(ios(11)); + +/// Top Safe Inset of the receiver in its current superview. +@property (readonly) KeepAttribute *keepTopSafeInset API_AVAILABLE(ios(11)); + +/// Bottom Safe Inset of the receiver in its current superview. Values are inverted to Bottom-to-Top direction. +@property (readonly) KeepAttribute *keepBottomSafeInset API_AVAILABLE(ios(11)); + + + +#pragma mark Superview Safe Insets: Convenience +/// Convenience methods for setting all safe insets at once. + +/// Grouped proxy attribute for Top, Bottom, Left and Right Safe Inset. +@property (readonly) KeepAttribute *keepSafeInsets API_AVAILABLE(ios(11)); + +/// Grouped proxy attribute for Left and Right Safe Inset. +@property (readonly) KeepAttribute *keepHorizontalSafeInsets API_AVAILABLE(ios(11)); + +/// Grouped proxy attribute for Top and Bottom Safe Inset. +@property (readonly) KeepAttribute *keepVerticalSafeInsets API_AVAILABLE(ios(11)); + +/// Sets custom Safe Insets using given priority. +- (void)keepSafeInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority API_AVAILABLE(ios(11)); + +/// Sets custom Safe Insets using Required priority. Use is discouraged. +- (void)keepSafeInsets:(KPEdgeInsets)insets API_AVAILABLE(ios(11)); + + + + + +#pragma mark - +#pragma mark Superview Margin Insets: Core +/// Attributes representing internal inset of the receive to its current superview, taking into account its margin insets. + +/// Left Margin Inset of the receiver in its current superview. +@property (readonly) KeepAttribute *keepLeftMarginInset; + +/// Right Margin Inset of the receiver in its current superview. Values are inverted to Right-to-Left direction. +@property (readonly) KeepAttribute *keepRightMarginInset; + +/// Leading Margin Inset of the receiver in its current superview. Depends on writing direction. +@property (readonly) KeepAttribute *keepLeadingMarginInset; + +/// Trailing Margin Inset of the receiver in its current superview. Values are inverted to Right-to-Left direction. Depends on writing direction. +@property (readonly) KeepAttribute *keepTrailingMarginInset; + +/// Top Margin Inset of the receiver in its current superview. +@property (readonly) KeepAttribute *keepTopMarginInset; + +/// Bottom Margin Inset of the receiver in its current superview. Values are inverted to Bottom-to-Top direction. +@property (readonly) KeepAttribute *keepBottomMarginInset; + + + +#pragma mark Superview Margin Insets: Convenience +/// Convenience methods for setting all margin insets at once. + +/// Grouped proxy attribute for Top, Bottom, Left and Right Margin Inset. +@property (readonly) KeepAttribute *keepMarginInsets; + +/// Grouped proxy attribute for Left and Right Margin Inset. +@property (readonly) KeepAttribute *keepHorizontalMarginInsets; + +/// Grouped proxy attribute for Top and Bottom Margin Inset. +@property (readonly) KeepAttribute *keepVerticalMarginInsets; + +/// Sets custom Margin Insets using given priority. +- (void)keepMarginInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority; + +/// Sets custom Margin Insets using Required priority. Use is discouraged. +- (void)keepMarginInsets:(KPEdgeInsets)insets; + + + + + +#pragma mark - +#pragma mark Center: Core +/// Attributes representing relative position of the receiver in its current superview. + +/// Horizontal Center of the receiver in its current superview (X axis). Value is multiplier of superview's width, so 0.5 is middle. +@property (readonly) KeepAttribute *keepHorizontalCenter; + +/// Vertical Center of the receiver in its current superview (Y axis). Value is multiplier of superview's height, so 0.5 is middle. +@property (readonly) KeepAttribute *keepVerticalCenter; + + + +#pragma mark Center: Convenience +/// Convenience methods for both center axis at once or with default values. + +/// Grouped proxy attribute for Horizontal and Vertical Center. Value is multiplier of superview's dimensions, so 0.5 is middle. +@property (readonly) KeepAttribute *keepCenter; + +/// Sets both Center axis to 0.5 with given priority. +- (void)keepCenteredWithPriority:(KeepPriority)priority; + +/// Sets both Center axis to 0.5 with Required priority. Use is discouraged. +- (void)keepCentered; + +/// Sets Horizontal Center axis to 0.5 with given priority. +- (void)keepHorizontallyCenteredWithPriority:(KeepPriority)priority; + +/// Sets Horizontal Center axis to 0.5 with Required priority. Use is discouraged. +- (void)keepHorizontallyCentered; + +/// Sets Vertical Center axis to 0.5 with given priority. +- (void)keepVerticallyCenteredWithPriority:(KeepPriority)priority; + +/// Sets Vertical Center axis to 0.5 with Required priority. Use is discouraged. +- (void)keepVerticallyCentered; + +/// Sets both center axis using given priority. Point values are multiplier of superview's dimensions, so 0.5 is middle. +- (void)keepCenter:(CGPoint)center priority:(KeepPriority)priority; + +/// Sets both center axis using Required priority. Use is discouraged. Point values are multiplier of superview's dimensions, so 0.5 is middle. +- (void)keepCenter:(CGPoint)center; + + + + + +#pragma mark - +#pragma mark Offsets: Core +/// Attributes representing offset (padding or distance) of two views. There attributes use opposite edges to create constraints. + +/// Left Offset to other view. Views must be in the same view hierarchy. +@property (readonly) KeepRelatedAttributeBlock keepLeftOffsetTo; + +/// Right Offset to other view. Views must be in the same view hierarchy. Identical to Left Offset in reversed direction. +@property (readonly) KeepRelatedAttributeBlock keepRightOffsetTo; + +/// Leading Offset to other view. Views must be in the same view hierarchy. Depends on writing direction. +@property (readonly) KeepRelatedAttributeBlock keepLeadingOffsetTo; + +/// Trailing Offset to other view. Views must be in the same view hierarchy. Identical to Leading Offset in reversed direction. Depends on writing direction. +@property (readonly) KeepRelatedAttributeBlock keepTrailingOffsetTo; + +/// Top Offset to other view. Views must be in the same view hierarchy. +@property (readonly) KeepRelatedAttributeBlock keepTopOffsetTo; + +/// Bottom Offset to other view. Views must be in the same view hierarchy. Identical to Top Offset in reversed direction. +@property (readonly) KeepRelatedAttributeBlock keepBottomOffsetTo; + +/// First Baseline Offset to other view’s Last Baseline. Not all views have baselines. +@property (nonatomic, readonly) KeepRelatedAttributeBlock keepFirstBaselineOffsetTo; + +/// Last Baseline Offset to other view’s First Baseline. Not all views have baselines. Identical to First Baseline Offset in reversed direction. +@property (nonatomic, readonly) KeepRelatedAttributeBlock keepLastBaselineOffsetTo; + + + + + +#pragma mark - +#pragma mark Alignments: Core +/// Attributes representing alignment of two views. You align view usually to 0 offset, but this can be changed. + +/// Left Alignment with other view. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. +@property (readonly) KeepRelatedAttributeBlock keepLeftAlignTo; + +/// Right Alignment with other view. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. Values are inverted to Right-to-Left direction. +@property (readonly) KeepRelatedAttributeBlock keepRightAlignTo; + +/// Leading Alignment with other view. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. Depends on writing direction. +@property (readonly) KeepRelatedAttributeBlock keepLeadingAlignTo; + +/// Trailing Alignment with other view. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. Values are inverted to Right-to-Left direction. Depends on writing direction. +@property (readonly) KeepRelatedAttributeBlock keepTrailingAlignTo; + +/// Top Alignment with other view. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. +@property (readonly) KeepRelatedAttributeBlock keepTopAlignTo; + +/// Bottom Alignment with other view. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. Values are inverted to Bottom-to-Top direction. +@property (readonly) KeepRelatedAttributeBlock keepBottomAlignTo; + +/// Vertical Center Alignment with other view, modifies the X position. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. +@property (readonly) KeepRelatedAttributeBlock keepVerticalAlignTo; + +/// Horizontal Center Alignment with other view, modifies the Y position. Views must be in the same view hierarchy. Value is offset of the receiver from the other view. +@property (readonly) KeepRelatedAttributeBlock keepHorizontalAlignTo; + +/// First Baseline Alignments of two views. Not all views have baselines. Values are inverted to Bottom-to-Top direction, so positive offset moves the receiver up. +@property (readonly) KeepRelatedAttributeBlock keepFirstBaselineAlignTo; + +/// Last Baseline Alignments of two views. Not all views have baselines. Values are inverted to Bottom-to-Top direction, so positive offset moves the receiver up. +@property (readonly) KeepRelatedAttributeBlock keepLastBaselineAlignTo; + + + +#pragma mark Alignments: Convenience +/// Convenience methods for setting multiple alignments at once. + +/// Grouped proxy attribute for Top, Left, Bottom and Right Alignment with other view. +@property (readonly) KeepRelatedAttributeBlock keepEdgeAlignTo; + +/// Grouped proxy attribute for Center X and Center Y Alignment with other view. +@property (readonly) KeepRelatedAttributeBlock keepCenterAlignTo; + + + + + +#pragma mark - +#pragma mark Compression & Hugging Convenience + +/// Convenience accessor for compression resistance priority in both axes. Primarily for setting. If they are different, lower is returned from getter. +@property KeepPriority keepCompressionResistance; + +/// Convenience accessor for compression resistance priority in X axis. +@property KeepPriority keepHorizontalCompressionResistance; + +/// Convenience accessor for compression resistance priority in Y axis. +@property KeepPriority keepVerticalCompressionResistance; + +/// Convenience accessor for hugging priority in both axes. Primarily for setting. If they are different, lower is returned from getter. +@property KeepPriority keepHuggingPriority; + +/// Convenience accessor for hugging priority in X axis. +@property KeepPriority keepHorizontalHuggingPriority; + +/// Convenience accessor for hugging priority in Y axis. +@property KeepPriority keepVerticalHuggingPriority; + + + + + +#pragma mark - +#pragma mark Animations +#if TARGET_OS_IOS +/// Animation methods allowing you to modify all above attributes (or even constraints directly) animatedly. All animations are scheduled on main queue with given or zero delay. The layout code itself is executed after the delay, which is different than in UIView animation methods. This behavior is needed, because of different nature of constraint-based layouting and allows you to schedule animations in the same update cycle as your main layout. + +/// Animate layout changes. Receiver automatically calls `-layoutIfNeeded` after the animation block. Animation is scheduled on Main Queue with zero delay, so the block not executed immediatelly. +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration layout:(void(^)(void))animations; + +/// Animate layout changes with delay. Receiver automatically calls `-layoutIfNeeded` after the animation block. Animation is scheduled on Main Queue with given delay. +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay layout:(void(^)(void))animations; + +/// Animate layout changes with completion. Receiver automatically calls `-layoutIfNeeded` after the animation block. Animation is scheduled on Main Queue with zero delay, so the block not executed immediatelly. +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration layout:(void(^)(void))animations completion:(void(^)(BOOL finished))completion; + +/// Animate layout changes with delay, options and completion. Receiver automatically calls `-layoutIfNeeded` after the animation block. Animation is scheduled on Main Queue with given delay. +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options layout:(void(^)(void))animations completion:(void(^)(BOOL finished))completion; + +/// Animate layout changes with spring behavior, delay, options and completion. Receiver automatically calls `-layoutIfNeeded` after the animation block. Animation is scheduled on Main Queue with given delay. +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options layout:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; + +/// Prevent animating layout changes in the block. Due to different nature of constraint-based layouting, this may not work as you may expect. +- (void)keepNotAnimated:(void (^)(void))layout; + +#endif // TARGET_OS_IOS + + + + + +@end diff --git a/ChatSDKKeepLayout/Sources/KeepView.m b/ChatSDKKeepLayout/Sources/KeepView.m new file mode 100644 index 00000000..23984eb2 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/KeepView.m @@ -0,0 +1,1025 @@ +// +// KeepView.m +// Keep Layout +// +// Created by Martin Kiss on 21.10.12. +// Copyright (c) 2012 iMartin Kiss. All rights reserved. +// + +#import "KeepView.h" +#import "KeepAttribute.h" +#import "KeepLayoutConstraint.h" +#import + + + + + +@implementation KPView (KeepLayout) + + + + + +#pragma mark Associations + + +- (KeepAttribute *)keep_selfAttributeForSelector:(SEL)selector creationBlock:(KeepAttribute *(^)(void))creationBlock { + KeepParameterAssert(selector); + KeepParameterAssert(creationBlock); + + KeepAttribute *attribute = objc_getAssociatedObject(self, selector); + + if ( ! attribute) { + attribute = creationBlock(); + objc_setAssociatedObject(self, selector, attribute, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return attribute; +} + + +- (KeepAttribute *)keep_superviewAttributeForSelector:(SEL)selector creationBlock:(KeepAttribute *(^)(void))creationBlock { + KeepParameterAssert(selector); + KeepParameterAssert(creationBlock); + + KeepAttribute *attribute = objc_getAssociatedObject(self, selector); + + if (attribute && ! [attribute isRelatedToView:self.superview]) { + [attribute deactivate]; + attribute = nil; + } + if ( ! attribute) { + attribute = creationBlock(); + objc_setAssociatedObject(self, selector, attribute, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return attribute; +} + + +- (KeepAttribute *)keep_relatedAttributeForSelector:(SEL)selector toView:(KPView *)relatedView creationBlock:(KeepAttribute *(^)(void))creationBlock { + KeepParameterAssert(selector); + KeepParameterAssert(relatedView); + + NSMapTable *attributesByRelatedView = objc_getAssociatedObject(self, selector); + if ( ! attributesByRelatedView) { + attributesByRelatedView = [NSMapTable weakToStrongObjectsMapTable]; + objc_setAssociatedObject(self, selector, attributesByRelatedView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + KeepAttribute *attribute = [attributesByRelatedView objectForKey:relatedView]; + if ( ! attribute && creationBlock) { + attribute = creationBlock(); + [attributesByRelatedView setObject:attribute forKey:relatedView]; + } + return attribute; +} + + +- (void)keep_clearAttribute:(SEL)selector { + KeepParameterAssert(selector); + + KeepAttribute *attribute = objc_getAssociatedObject(self, selector); + [attribute deactivate]; + + objc_setAssociatedObject(self, selector, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + + + + + +#pragma mark Dimensions + + +- (KeepAttribute *)keep_dimensionForSelector:(SEL)selector dimensionAttribute:(NSLayoutAttribute)dimensionAttribute name:(NSString *)name { + KeepParameterAssert(selector); + KeepParameterAssert(dimensionAttribute == NSLayoutAttributeWidth + || dimensionAttribute == NSLayoutAttributeHeight); + KeepParameterAssert(name); + + return [self keep_selfAttributeForSelector:selector creationBlock:^KeepAttribute *{ + KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self + layoutAttribute:dimensionAttribute + relatedView:nil + relatedLayoutAttribute:NSLayoutAttributeNotAnAttribute + coefficient:1] + name:@"%@ of <%@ %p>", name, self.class, self]; + self.translatesAutoresizingMaskIntoConstraints = NO; + return attribute; + }]; +} + + +- (KeepAttribute *)keep_dimensionForSelector:(SEL)selector dimensionAttribute:(NSLayoutAttribute)dimensionAttribute relatedView:(KPView *)relatedView name:(NSString *)name { + KeepParameterAssert(selector); + KeepParameterAssert(dimensionAttribute == NSLayoutAttributeWidth + || dimensionAttribute == NSLayoutAttributeHeight); + KeepParameterAssert(relatedView); + KeepParameterAssert(name); + + return [self keep_relatedAttributeForSelector:selector toView:relatedView creationBlock:^KeepAttribute *{ + KeepAttribute *attribute = [[KeepMultiplierAttribute alloc] initWithView:self + layoutAttribute:dimensionAttribute + relatedView:relatedView + relatedLayoutAttribute:dimensionAttribute + coefficient:1]; + [attribute name:@"%@ of <%@ %p> to <%@ %p>", name, self.class, self, relatedView.class, relatedView]; + self.translatesAutoresizingMaskIntoConstraints = NO; + relatedView.translatesAutoresizingMaskIntoConstraints = NO; + // Establish inverse relation + [relatedView keep_relatedAttributeForSelector:_cmd toView:self creationBlock:^KeepAttribute *{ + return attribute; + }]; + return attribute; + }]; +} + + +- (KeepAttribute *)keepWidth { + return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeWidth name:@"width"]; +} + + +- (KeepAttribute *)keepHeight { + return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeHeight name:@"height"]; +} + + +- (KeepAttribute *)keepSize { + return [[KeepAttribute group: + self.keepWidth, + self.keepHeight, + nil] name:@"size of <%@ %p>", self.class, self]; +} + + +- (void)keepSize:(CGSize)size { + [self keepSize:size priority:KeepPriorityRequired]; +} + + +- (void)keepSize:(CGSize)size priority:(KeepPriority)priority { + self.keepWidth.equal = KeepValueMake(size.width, priority); + self.keepHeight.equal = KeepValueMake(size.height, priority); +} + + +- (KeepAttribute *)keepAspectRatio { + return [self keep_selfAttributeForSelector:_cmd creationBlock:^KeepAttribute *{ + KeepAttribute *attribute = [[KeepMultiplierAttribute alloc] initWithView:self + layoutAttribute:NSLayoutAttributeWidth + relatedView:self + relatedLayoutAttribute:NSLayoutAttributeHeight + coefficient:1]; + [attribute name:@"aspect ratio of <%@ %p>", self.class, self]; + self.translatesAutoresizingMaskIntoConstraints = NO; + return attribute; + }]; +} + + +- (KeepRelatedAttributeBlock)keepWidthTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeWidth relatedView:view name:@"width"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepHeightTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeHeight relatedView:view name:@"height"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepSizeTo { + return ^KeepAttribute *(KPView *view) { + return [[KeepAttribute group: + self.keepWidthTo(view), + self.keepHeightTo(view), + nil] name:@"size of <%@ %p> to <%@ %p>", self.class, self, view.class, view]; + }; +} + + + + + +#pragma mark Supreview Insets + + +- (KeepAttribute *)keep_insetForSelector:(SEL)selector useSafe:(BOOL)useSafeInsets edgeAttribute:(NSLayoutAttribute)edgeAttribute coefficient:(CGFloat)coefficient name:(NSString *)name { + KeepParameterAssert(selector); + KeepParameterAssert( edgeAttribute == NSLayoutAttributeLeft + || edgeAttribute == NSLayoutAttributeRight + || edgeAttribute == NSLayoutAttributeTop + || edgeAttribute == NSLayoutAttributeBottom + || edgeAttribute == NSLayoutAttributeLeading + || edgeAttribute == NSLayoutAttributeTrailing + || edgeAttribute == NSLayoutAttributeLeftMargin + || edgeAttribute == NSLayoutAttributeRightMargin + || edgeAttribute == NSLayoutAttributeTopMargin + || edgeAttribute == NSLayoutAttributeBottomMargin + || edgeAttribute == NSLayoutAttributeLeadingMargin + || edgeAttribute == NSLayoutAttributeTrailingMargin + || edgeAttribute == NSLayoutAttributeFirstBaseline + || edgeAttribute == NSLayoutAttributeLastBaseline + ); + KeepParameterAssert(name); + KeepAssert(self.superview, @"Calling %@ allowed only when superview exists", NSStringFromSelector(selector)); + + return [self keep_superviewAttributeForSelector:selector creationBlock:^KeepAttribute *{ + NSDictionary *nonMarginAttributes = @{ + @(NSLayoutAttributeLeftMargin): @(NSLayoutAttributeLeft), + @(NSLayoutAttributeRightMargin): @(NSLayoutAttributeRight), + @(NSLayoutAttributeTopMargin): @(NSLayoutAttributeTop), + @(NSLayoutAttributeBottomMargin): @(NSLayoutAttributeBottom), + @(NSLayoutAttributeLeadingMargin): @(NSLayoutAttributeLeading), + @(NSLayoutAttributeTrailingMargin): @(NSLayoutAttributeTrailing), + }; + NSDictionary *nonBaselineAttributes = @{ + @(NSLayoutAttributeFirstBaseline): @(NSLayoutAttributeTop), + @(NSLayoutAttributeLastBaseline): @(NSLayoutAttributeBottom), + }; + NSLayoutAttribute superviewEdgeAttribute = [[nonBaselineAttributes objectForKey:@(edgeAttribute)] integerValue] ?: edgeAttribute; + NSLayoutAttribute selfEdgeAttribute = [[nonMarginAttributes objectForKey:@(edgeAttribute)] integerValue] ?: edgeAttribute; + + id related = self.superview; + if (useSafeInsets) { + if (@available(iOS 11, *)) { + related = self.superview.safeAreaLayoutGuide; + } + } + + KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self + layoutAttribute:selfEdgeAttribute + relatedView:related + relatedLayoutAttribute:superviewEdgeAttribute + coefficient:coefficient] + name:@"%@ of <%@ %p> to superview <%@ %p>", name, self.class, self, self.superview.class, self.superview]; + self.translatesAutoresizingMaskIntoConstraints = NO; + return attribute; + }]; +} + + +- (KeepAttribute *)keepLeftInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeLeft coefficient:1 name:@"left inset"]; +} + + +- (KeepAttribute *)keepRightInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeRight coefficient:-1 name:@"right inset"]; +} + + +- (KeepAttribute *)keepLeadingInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeLeading coefficient:1 name:@"leading inset"]; +} + + +- (KeepAttribute *)keepTrailingInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeTrailing coefficient:-1 name:@"trailing inset"]; +} + + +- (KeepAttribute *)keepTopInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeTop coefficient:1 name:@"top inset"]; +} + + +- (KeepAttribute *)keepBottomInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeBottom coefficient:-1 name:@"bottom inset"]; +} + + +- (KeepAttribute *)keepFirstBaselineInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeFirstBaseline coefficient:1 name:@"first baseline inset"]; +} + + +- (KeepAttribute *)keepLastBaselineInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeLastBaseline coefficient:-1 name:@"last baseline inset"]; +} + + +- (KeepAttribute *)keepInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepTopInset, + self.keepBottomInset, + self.keepLeftInset, + self.keepRightInset ]] + name:@"all insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (KeepAttribute *)keepHorizontalInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepLeftInset, + self.keepRightInset ]] + name:@"horizontal insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (KeepAttribute *)keepVerticalInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepTopInset, + self.keepBottomInset ]] + name:@"vertical insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (void)keepInsets:(KPEdgeInsets)insets { + [self keepInsets:insets priority:KeepPriorityRequired]; +} + + +- (void)keepInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority { + self.keepLeftInset.equal = KeepValueMake(insets.left, priority); + self.keepRightInset.equal = KeepValueMake(insets.right, priority); + self.keepTopInset.equal = KeepValueMake(insets.top, priority); + self.keepBottomInset.equal = KeepValueMake(insets.bottom, priority); +} + + + + + +#pragma mark Superview Safe Insets + + +- (KeepAttribute *)keepLeftSafeInset { + return [self keep_insetForSelector:_cmd useSafe:YES edgeAttribute:NSLayoutAttributeLeft coefficient:1 name:@"left safe inset"]; +} + + +- (KeepAttribute *)keepRightSafeInset { + return [self keep_insetForSelector:_cmd useSafe:YES edgeAttribute:NSLayoutAttributeRight coefficient:-1 name:@"right safe inset"]; +} + + +- (KeepAttribute *)keepLeadingSafeInset { + return [self keep_insetForSelector:_cmd useSafe:YES edgeAttribute:NSLayoutAttributeLeading coefficient:1 name:@"leading safe inset"]; +} + + +- (KeepAttribute *)keepTrailingSafeInset { + return [self keep_insetForSelector:_cmd useSafe:YES edgeAttribute:NSLayoutAttributeTrailing coefficient:-1 name:@"trailing safe inset"]; +} + + +- (KeepAttribute *)keepTopSafeInset { + return [self keep_insetForSelector:_cmd useSafe:YES edgeAttribute:NSLayoutAttributeTop coefficient:1 name:@"top safe inset"]; +} + + +- (KeepAttribute *)keepBottomSafeInset { + return [self keep_insetForSelector:_cmd useSafe:YES edgeAttribute:NSLayoutAttributeBottom coefficient:-1 name:@"bottom safe inset"]; +} + + +- (KeepAttribute *)keepSafeInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepTopSafeInset, + self.keepBottomSafeInset, + self.keepLeftSafeInset, + self.keepRightSafeInset ]] + name:@"all safe insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (KeepAttribute *)keepHorizontalSafeInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepLeftSafeInset, + self.keepRightSafeInset ]] + name:@"horizontal safe insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (KeepAttribute *)keepVerticalSafeInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepTopSafeInset, + self.keepBottomSafeInset ]] + name:@"vertical safe insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (void)keepSafeInsets:(KPEdgeInsets)insets { + [self keepSafeInsets:insets priority:KeepPriorityRequired]; +} + + +- (void)keepSafeInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority { + self.keepLeftSafeInset.equal = KeepValueMake(insets.left, priority); + self.keepRightSafeInset.equal = KeepValueMake(insets.right, priority); + self.keepTopSafeInset.equal = KeepValueMake(insets.top, priority); + self.keepBottomSafeInset.equal = KeepValueMake(insets.bottom, priority); +} + + + + + +#pragma mark Superview Margin Insets + + +- (KeepAttribute *)keepLeftMarginInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeLeftMargin coefficient:1 name:@"left margin inset"]; +} + + +- (KeepAttribute *)keepRightMarginInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeRightMargin coefficient:-1 name:@"right margin inset"]; +} + + +- (KeepAttribute *)keepLeadingMarginInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeLeadingMargin coefficient:1 name:@"leading margin inset"]; +} + + +- (KeepAttribute *)keepTrailingMarginInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeTrailingMargin coefficient:-1 name:@"trailing margin inset"]; +} + + +- (KeepAttribute *)keepTopMarginInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeTopMargin coefficient:1 name:@"top margin inset"]; +} + + +- (KeepAttribute *)keepBottomMarginInset { + return [self keep_insetForSelector:_cmd useSafe:NO edgeAttribute:NSLayoutAttributeBottomMargin coefficient:-1 name:@"bottom margin inset"]; +} + + +- (KeepAttribute *)keepMarginInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepTopMarginInset, + self.keepBottomMarginInset, + self.keepLeftMarginInset, + self.keepRightMarginInset ]] + name:@"all margin insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (KeepAttribute *)keepHorizontalMarginInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepLeftMarginInset, + self.keepRightMarginInset ]] + name:@"horizontal margin insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (KeepAttribute *)keepVerticalMarginInsets { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepTopMarginInset, + self.keepBottomMarginInset ]] + name:@"vertical margin insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (void)keepMarginInsets:(KPEdgeInsets)insets { + [self keepMarginInsets:insets priority:KeepPriorityRequired]; +} + + +- (void)keepMarginInsets:(KPEdgeInsets)insets priority:(KeepPriority)priority { + self.keepLeftMarginInset.equal = KeepValueMake(insets.left, priority); + self.keepRightMarginInset.equal = KeepValueMake(insets.right, priority); + self.keepTopMarginInset.equal = KeepValueMake(insets.top, priority); + self.keepBottomMarginInset.equal = KeepValueMake(insets.bottom, priority); +} + + + + + +#pragma mark Center + + +- (KeepAttribute *)keep_centerForSelector:(SEL)selector centerAttribute:(NSLayoutAttribute)centerAttribute name:(NSString *)name { + KeepParameterAssert(selector); + KeepParameterAssert(centerAttribute == NSLayoutAttributeCenterX + || centerAttribute == NSLayoutAttributeCenterY); + KeepParameterAssert(name); + KeepAssert(self.superview, @"Calling %@ allowed only when superview exists", NSStringFromSelector(selector)); + + return [self keep_superviewAttributeForSelector:selector creationBlock:^KeepAttribute *{ + KeepAttribute *attribute = [[[KeepMultiplierAttribute alloc] initWithView:self + layoutAttribute:centerAttribute + relatedView:self.superview + relatedLayoutAttribute:centerAttribute + coefficient:2] + name:@"%@ of <%@ %p> in superview <%@ %p>", name, self.class, self, self.superview.class, self.superview]; + self.translatesAutoresizingMaskIntoConstraints = NO; + return attribute; + }]; +} + + +- (KeepAttribute *)keepHorizontalCenter { + return [self keep_centerForSelector:_cmd centerAttribute:NSLayoutAttributeCenterX name:@"horizontal center"]; +} + + +- (KeepAttribute *)keepVerticalCenter { + return [self keep_centerForSelector:_cmd centerAttribute:NSLayoutAttributeCenterY name:@"vertical center"]; +} + + +- (KeepAttribute *)keepCenter { + return [[[KeepGroupAttribute alloc] initWithAttributes:@[ + self.keepHorizontalCenter, + self.keepVerticalCenter ]] + name:@"center of <%@ %p> in superview <%@ %p>", self.class, self, self.superview.class, self.superview]; +} + + +- (void)keepCentered { + [self keepCenteredWithPriority:KeepPriorityRequired]; +} + + +- (void)keepCenteredWithPriority:(KeepPriority)priority { + [self keepCenter:CGPointMake(0.5, 0.5) priority:priority]; +} + + +- (void)keepHorizontallyCentered { + [self keepHorizontallyCenteredWithPriority:KeepPriorityRequired]; +} + + +- (void)keepHorizontallyCenteredWithPriority:(KeepPriority)priority { + self.keepHorizontalCenter.equal = KeepValueMake(0.5, priority); +} + + +- (void)keepVerticallyCentered { + [self keepVerticallyCenteredWithPriority:KeepPriorityRequired]; +} + + +- (void)keepVerticallyCenteredWithPriority:(KeepPriority)priority { + self.keepVerticalCenter.equal = KeepValueMake(0.5, priority); +} + + +- (void)keepCenter:(CGPoint)center { + [self keepCenter:center priority:KeepPriorityRequired]; +} + + +- (void)keepCenter:(CGPoint)center priority:(KeepPriority)priority { + self.keepHorizontalCenter.equal = KeepValueMake(center.x, priority); + self.keepVerticalCenter.equal = KeepValueMake(center.y, priority); +} + + + + + +#pragma mark Offsets + + +- (KeepAttribute *)keep_offsetForSelector:(SEL)selector edgeAttribute:(NSLayoutAttribute)edgeAttribute relatedView:(KPView *)relatedView name:(NSString *)name { + KeepParameterAssert(selector); + KeepParameterAssert( edgeAttribute == NSLayoutAttributeLeft + || edgeAttribute == NSLayoutAttributeRight + || edgeAttribute == NSLayoutAttributeTop + || edgeAttribute == NSLayoutAttributeBottom + || edgeAttribute == NSLayoutAttributeLeading + || edgeAttribute == NSLayoutAttributeTrailing + || edgeAttribute == NSLayoutAttributeFirstBaseline + || edgeAttribute == NSLayoutAttributeLastBaseline); + KeepParameterAssert(relatedView); + KeepParameterAssert(name); + + return [self keep_relatedAttributeForSelector:selector toView:relatedView creationBlock:^KeepAttribute *{ + NSDictionary *oppositeEdges = @{ + @(NSLayoutAttributeLeft): @(NSLayoutAttributeRight), + @(NSLayoutAttributeRight): @(NSLayoutAttributeLeft), + @(NSLayoutAttributeTop): @(NSLayoutAttributeBottom), + @(NSLayoutAttributeBottom): @(NSLayoutAttributeTop), + @(NSLayoutAttributeLeading): @(NSLayoutAttributeTrailing), + @(NSLayoutAttributeTrailing): @(NSLayoutAttributeLeading), + @(NSLayoutAttributeFirstBaseline): @(NSLayoutAttributeLastBaseline), + @(NSLayoutAttributeLastBaseline): @(NSLayoutAttributeFirstBaseline), + }; + KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self + layoutAttribute:edgeAttribute + relatedView:relatedView + relatedLayoutAttribute:[[oppositeEdges objectForKey:@(edgeAttribute)] integerValue] + coefficient:1] + name:@"%@ of <%@ %p> to <%@ %p>", name, self.class, self, relatedView.class, relatedView]; + self.translatesAutoresizingMaskIntoConstraints = NO; + relatedView.translatesAutoresizingMaskIntoConstraints = NO; + return attribute; + }]; +} + + +- (KeepRelatedAttributeBlock)keepLeftOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_offsetForSelector:_cmd edgeAttribute:NSLayoutAttributeLeft relatedView:view name:@"left offset"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepRightOffsetTo { + return ^KeepAttribute *(KPView *view) { + return view.keepLeftOffsetTo(self); + }; +} + + +- (KeepRelatedAttributeBlock)keepLeadingOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_offsetForSelector:_cmd edgeAttribute:NSLayoutAttributeLeading relatedView:view name:@"leading offset"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepTrailingOffsetTo { + return ^KeepAttribute *(KPView *view) { + return view.keepLeadingOffsetTo(self); + }; +} + + +- (KeepRelatedAttributeBlock)keepTopOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_offsetForSelector:_cmd edgeAttribute:NSLayoutAttributeTop relatedView:view name:@"top offset"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepBottomOffsetTo { + return ^KeepAttribute *(KPView *view) { + return view.keepTopOffsetTo(self); + }; +} + + +- (KeepRelatedAttributeBlock)keepFirstBaselineOffsetTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_offsetForSelector:_cmd edgeAttribute:NSLayoutAttributeFirstBaseline relatedView:view name:@"first baseline offset"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepLastBaselineOffsetTo { + return ^KeepAttribute *(KPView *view) { + return view.keepFirstBaselineOffsetTo(self); + }; +} + + + + + +#pragma mark Alignments + + +- (KeepAttribute *)keep_alignForSelector:(SEL)selector alignAttribute:(NSLayoutAttribute)alignAttribute relatedView:(KPView *)relatedView coefficient:(CGFloat)coefficient name:(NSString *)name { + KeepParameterAssert(selector); + KeepParameterAssert(alignAttribute == NSLayoutAttributeLeft + || alignAttribute == NSLayoutAttributeRight + || alignAttribute == NSLayoutAttributeTop + || alignAttribute == NSLayoutAttributeBottom + || alignAttribute == NSLayoutAttributeLeading + || alignAttribute == NSLayoutAttributeTrailing + || alignAttribute == NSLayoutAttributeCenterX + || alignAttribute == NSLayoutAttributeBaseline + || alignAttribute == NSLayoutAttributeFirstBaseline + || alignAttribute == NSLayoutAttributeLastBaseline + || alignAttribute == NSLayoutAttributeCenterY); + KeepParameterAssert(relatedView); + KeepParameterAssert(name); + + return [self keep_relatedAttributeForSelector:selector toView:relatedView creationBlock:^KeepAttribute *{ + KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self + layoutAttribute:alignAttribute + relatedView:relatedView + relatedLayoutAttribute:alignAttribute + coefficient:coefficient] + name:@"%@ of <%@ %p> to <%@ %p>", name, self.class, self, relatedView.class, relatedView]; + self.translatesAutoresizingMaskIntoConstraints = NO; + relatedView.translatesAutoresizingMaskIntoConstraints = NO; + // Establish inverse attribute + [relatedView keep_relatedAttributeForSelector:selector toView:self creationBlock:^KeepAttribute *{ + return attribute; + }]; + return attribute; + }]; +} + + +- (KeepRelatedAttributeBlock)keepLeftAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeLeft relatedView:view coefficient:1 name:@"left alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepRightAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeRight relatedView:view coefficient:-1 name:@"right alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepLeadingAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeLeading relatedView:view coefficient:1 name:@"leading alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepTrailingAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeTrailing relatedView:view coefficient:-1 name:@"trailing alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepTopAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeTop relatedView:view coefficient:1 name:@"top alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepBottomAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeBottom relatedView:view coefficient:-1 name:@"bottom alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepEdgeAlignTo { + return ^KeepAttribute *(KPView *view) { + return [KeepAttribute group: + self.keepTopAlignTo(view), + self.keepLeftAlignTo(view), + self.keepRightAlignTo(view), + self.keepBottomAlignTo(view), + nil]; + }; +} + + +- (KeepRelatedAttributeBlock)keepCenterAlignTo { + return ^KeepAttribute *(KPView *view) { + return [KeepAttribute group: + self.keepVerticalAlignTo(view), + self.keepHorizontalAlignTo(view), + nil]; + }; +} + + +- (KeepRelatedAttributeBlock)keepVerticalAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeCenterX relatedView:view coefficient:1 name:@"vertical center alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepHorizontalAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeCenterY relatedView:view coefficient:1 name:@"horizontal center alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepFirstBaselineAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeFirstBaseline relatedView:view coefficient:-1 name:@"first baseline alignment"]; + }; +} + + +- (KeepRelatedAttributeBlock)keepLastBaselineAlignTo { + return ^KeepAttribute *(KPView *view) { + return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeLastBaseline relatedView:view coefficient:-1 name:@"last baseline alignment"]; + }; +} + + + + + +#pragma mark Compression & Hugging Convenience + + +- (KeepPriority)keepCompressionResistance { + return MIN(self.keepHorizontalCompressionResistance, self.keepVerticalCompressionResistance); +} + + +- (void)setKeepCompressionResistance:(KeepPriority)priority { + self.keepHorizontalCompressionResistance = priority; + self.keepVerticalCompressionResistance = priority; +} + + +- (KeepPriority)keepHorizontalCompressionResistance { +#if TARGET_OS_IOS + return [self contentCompressionResistancePriorityForAxis:UILayoutConstraintAxisHorizontal]; +#else + return [self contentCompressionResistancePriorityForOrientation:NSLayoutConstraintOrientationHorizontal]; +#endif +} + + +- (void)setKeepHorizontalCompressionResistance:(KeepPriority)priority { +#if TARGET_OS_IOS + [self setContentCompressionResistancePriority:priority forAxis:UILayoutConstraintAxisHorizontal]; +#else + [self setContentCompressionResistancePriority:priority forOrientation:NSLayoutConstraintOrientationHorizontal]; +#endif +} + + +- (KeepPriority)keepVerticalCompressionResistance { +#if TARGET_OS_IOS + return [self contentCompressionResistancePriorityForAxis:UILayoutConstraintAxisVertical]; +#else + return [self contentCompressionResistancePriorityForOrientation:NSLayoutConstraintOrientationVertical]; +#endif +} + + +- (void)setKeepVerticalCompressionResistance:(KeepPriority)priority { +#if TARGET_OS_IOS + [self setContentCompressionResistancePriority:priority forAxis:UILayoutConstraintAxisVertical]; +#else + [self setContentCompressionResistancePriority:priority forOrientation:NSLayoutConstraintOrientationVertical]; +#endif +} + + +- (KeepPriority)keepHuggingPriority { + return MIN(self.keepHorizontalHuggingPriority, self.keepVerticalHuggingPriority); +} + + +- (void)setKeepHuggingPriority:(KeepPriority)priority { + self.keepHorizontalHuggingPriority = priority; + self.keepVerticalHuggingPriority = priority; +} + + +- (KeepPriority)keepHorizontalHuggingPriority { +#if TARGET_OS_IOS + return [self contentHuggingPriorityForAxis:UILayoutConstraintAxisHorizontal]; +#else + return [self contentHuggingPriorityForOrientation:NSLayoutConstraintOrientationHorizontal]; +#endif +} + + +- (void)setKeepHorizontalHuggingPriority:(KeepPriority)priority { +#if TARGET_OS_IOS + [self setContentHuggingPriority:priority forAxis:UILayoutConstraintAxisHorizontal]; +#else + [self setContentHuggingPriority:priority forOrientation:NSLayoutConstraintOrientationHorizontal]; +#endif +} + + +- (KeepPriority)keepVerticalHuggingPriority { +#if TARGET_OS_IOS + return [self contentHuggingPriorityForAxis:UILayoutConstraintAxisVertical]; +#else + return [self contentHuggingPriorityForOrientation:NSLayoutConstraintOrientationVertical]; +#endif +} + + +- (void)setKeepVerticalHuggingPriority:(KeepPriority)priority { +#if TARGET_OS_IOS + [self setContentHuggingPriority:priority forAxis:UILayoutConstraintAxisVertical]; +#else + [self setContentHuggingPriority:priority forOrientation:NSLayoutConstraintOrientationVertical]; +#endif +} + + + + + +#pragma mark Animating Constraints +#if TARGET_OS_IOS + + +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration layout:(void(^)(void))animations { + KeepParameterAssert(duration >= 0); + KeepParameterAssert(animations); + + [self keep_animationPerformWithDuration:duration delay:0 block:^{ + [UIView animateWithDuration:duration + animations:^{ + animations(); + [self layoutIfNeeded]; + }]; + }]; +} + + +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay layout:(void(^)(void))animations { + KeepParameterAssert(duration >= 0); + KeepParameterAssert(delay >= 0); + KeepParameterAssert(animations); + + [self keep_animationPerformWithDuration:duration delay:delay block:^{ + [UIView animateWithDuration:duration + animations:^{ + animations(); + [self layoutIfNeeded]; + }]; + }]; +} + + +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration layout:(void (^)(void))animations completion:(void (^)(BOOL))completion { + KeepParameterAssert(duration >= 0); + KeepParameterAssert(animations); + + [self keep_animationPerformWithDuration:duration delay:0 block:^{ + [UIView animateWithDuration:duration + animations:^{ + animations(); + [self layoutIfNeeded]; + } + completion:completion]; + }]; +} + + +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options layout:(void(^)(void))animations completion:(void(^)(BOOL finished))completion { + KeepParameterAssert(duration >= 0); + KeepParameterAssert(delay >= 0); + KeepParameterAssert(animations); + + [self keep_animationPerformWithDuration:duration delay:delay block:^{ + [UIView animateWithDuration:duration + delay:0 + options:options + animations:^{ + animations(); + [self layoutIfNeeded]; + } + completion:completion]; + }]; +} + + +- (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options layout:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { + KeepParameterAssert(duration >= 0); + KeepParameterAssert(delay >= 0); + KeepParameterAssert(animations); + + [self keep_animationPerformWithDuration:duration delay:delay block:^{ + [UIView animateWithDuration:duration + delay:0 + usingSpringWithDamping:dampingRatio + initialSpringVelocity:velocity + options:options + animations:^{ + animations(); + [self layoutIfNeeded]; + } + completion:completion]; + }]; +} + + +- (void)keep_animationPerformWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay block:(void(^)(void))block { + if (duration > 0 || delay > 0) { + [[NSOperationQueue mainQueue] performSelector:@selector(addOperationWithBlock:) + withObject:block + afterDelay:delay + inModes:@[NSRunLoopCommonModes]]; + } + else { + block(); + } +} + + +- (void)keepNotAnimated:(void (^)(void))layout { + [UIView performWithoutAnimation:^{ + layout(); + [self layoutIfNeeded]; + }]; +} + +#endif // TARGET_OS_IOS + + + + + +@end diff --git a/ChatSDKKeepLayout/Sources/UIScrollView+KeepLayout.h b/ChatSDKKeepLayout/Sources/UIScrollView+KeepLayout.h new file mode 100644 index 00000000..7d54ec1f --- /dev/null +++ b/ChatSDKKeepLayout/Sources/UIScrollView+KeepLayout.h @@ -0,0 +1,24 @@ +// +// UIScrollView+KeepLayout.h +// Keep Layout +// +// Created by Martin Kiss on 13.11.14. +// Copyright (c) 2014 Martin Kiss. All rights reserved. +// + +#import + + + +@interface UIScrollView (KeepLayout) + + +//! When set to YES, a view with special constraints is installed in the receiver to prevent expanding content width. +@property (setter=keepHorizontalScrollDisabled:) BOOL keepHorizontalScrollDisabled; +//! When set to YES, a view with special constraints is installed in the receiver to prevent expanding content height. +@property (setter=keepVerticalScrollDisabled:) BOOL keepVerticalScrollDisabled; + + +@end + + diff --git a/ChatSDKKeepLayout/Sources/UIScrollView+KeepLayout.m b/ChatSDKKeepLayout/Sources/UIScrollView+KeepLayout.m new file mode 100644 index 00000000..368893a3 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/UIScrollView+KeepLayout.m @@ -0,0 +1,95 @@ +// +// UIScrollView+KeepLayout.m +// Keep Layout +// +// Created by Martin Kiss on 13.11.14. +// Copyright (c) 2014 Martin Kiss. All rights reserved. +// + +#import +#import "UIScrollView+KeepLayout.h" +#import "KeepView.h" +#import "KeepAttribute.h" + + + + + +@interface KeepScrollDisablingView : UIView + +@end + + + + + +@implementation UIScrollView (KeepLayout) + + + +- (UIView *)keepScrollDisablingView { + UIView *disablingView = objc_getAssociatedObject(self, _cmd); + if ( ! disablingView) { + disablingView = [KeepScrollDisablingView new]; + disablingView.hidden = YES; + disablingView.userInteractionEnabled = NO; + disablingView.backgroundColor = [UIColor clearColor]; + [self insertSubview:disablingView atIndex:0]; + + disablingView.keepInsets.equal = 0; + } + return disablingView; +} + + +- (BOOL)keepHorizontalScrollDisabled { + UIView *disablingView = [self keepScrollDisablingView]; + KeepValue value = disablingView.keepWidthTo(self).equal; + return ! KeepValueIsNone(value); +} + + +- (void)keepHorizontalScrollDisabled:(BOOL)disabled { + UIView *disablingView = [self keepScrollDisablingView]; + disablingView.keepWidthTo(self).equal = (disabled? 1 : KeepNone); +} + + +- (BOOL)keepVerticalScrollDisabled { + UIView *disablingView = [self keepScrollDisablingView]; + KeepValue value = disablingView.keepHeightTo(self).equal; + return ! KeepValueIsNone(value); +} + + +- (void)keepVerticalScrollDisabled:(BOOL)disabled { + UIView *disablingView = [self keepScrollDisablingView]; + disablingView.keepHeightTo(self).equal = (disabled? 1 : KeepNone); +} + + + +@end + + + + + +@implementation KeepScrollDisablingView + + +- (void)addSubview:(UIView *)view { + KeepAssert(NO, @"This is special view that doesn't support adding subviews. Get over it."); + [super addSubview:view]; +} + + +- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index { + KeepAssert(NO, @"This is special view that doesn't support adding subviews. Get over it."); + [super addSubview:view]; +} + + +@end + + diff --git a/ChatSDKKeepLayout/Sources/UIViewController+KeepLayout.h b/ChatSDKKeepLayout/Sources/UIViewController+KeepLayout.h new file mode 100644 index 00000000..f5f45938 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/UIViewController+KeepLayout.h @@ -0,0 +1,22 @@ +// +// UIViewController+KeepLayout.h +// Keep Layout +// +// Created by Martin Kiss on 4.6.14. +// Copyright (c) 2014 Martin Kiss. All rights reserved. +// + +#import + + + +@interface UIViewController (KeepLayout) + + +/// Lazy-loaded hidden view that can be used to align sibling views. This layoutView is aligned with view controller's safeAreaInsets on iOS 11 and topLayoutGuide and bottomLayoutGuide on older iOS versions. Don't add any subviews into this view, it's invisible. +@property (readonly) UIView *keepLayoutView API_DEPRECATED("Use .keepSafeInset and others", ios(7, 11)); + + +@end + + diff --git a/ChatSDKKeepLayout/Sources/UIViewController+KeepLayout.m b/ChatSDKKeepLayout/Sources/UIViewController+KeepLayout.m new file mode 100644 index 00000000..56e5ec68 --- /dev/null +++ b/ChatSDKKeepLayout/Sources/UIViewController+KeepLayout.m @@ -0,0 +1,95 @@ +// +// UIViewController+KeepLayout.m +// Keep Layout +// +// Created by Martin Kiss on 4.6.14. +// Copyright (c) 2014 Martin Kiss. All rights reserved. +// + +#import +#import "UIViewController+KeepLayout.h" +#import "KeepLayoutConstraint.h" +#import "KeepView.h" +#import "KeepAttribute.h" + + + + + +@interface KeepLayoutGuidesView : UIView @end + + + + + +@implementation UIViewController (KeepLayout) + + + +- (UIView *)keepLayoutView { + UIView *layoutView = objc_getAssociatedObject(self, _cmd); + if (layoutView) return layoutView; + + layoutView = [KeepLayoutGuidesView new]; + layoutView.hidden = YES; + layoutView.userInteractionEnabled = NO; + layoutView.backgroundColor = [UIColor clearColor]; + [self.view insertSubview:layoutView atIndex:0]; + + if (@available(iOS 11.0, *)) { + layoutView.keepSafeInsets.equal = 0; + } + else { + layoutView.keepHorizontalInsets.equal = 0; + + KeepLayoutConstraint *topAlign = [KeepLayoutConstraint constraintWithItem:layoutView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:0]; + [topAlign name:@"align top of <%@ %p> to top layout guide of <%@ %p>", layoutView.class, layoutView, self.class, self]; + + KeepLayoutConstraint *bottomAlign = [KeepLayoutConstraint constraintWithItem:layoutView + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.bottomLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:0]; + [bottomAlign name:@"align bottom of <%@ %p> to bottom of safe area layout guide of <%@ %p>", layoutView.class, layoutView, self.class, self]; + + [KeepLayoutConstraint activateConstraints:@[topAlign, bottomAlign]]; + } + + objc_setAssociatedObject(self, _cmd, layoutView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + return layoutView; +} + + + +@end + + + + + +@implementation KeepLayoutGuidesView + + +- (void)addSubview:(UIView *)view { + KeepAssert(NO, @"This is special view that doesn't support adding subviews. Get over it."); + [super addSubview:view]; +} + + +- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index { + KeepAssert(NO, @"This is special view that doesn't support adding subviews. Get over it."); + [super addSubview:view]; +} + + +@end + + diff --git a/ChatSDKUI/Assets/.gitkeep b/ChatSDKUI/Assets/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/ChatSDKUI/Assets/chat_icon@2x.png b/ChatSDKUI/Assets/chat_icon@2x.png new file mode 100755 index 00000000..0341c538 Binary files /dev/null and b/ChatSDKUI/Assets/chat_icon@2x.png differ diff --git a/ChatSDKUI/Assets/chat_icon@3x.png b/ChatSDKUI/Assets/chat_icon@3x.png new file mode 100755 index 00000000..ef17f766 Binary files /dev/null and b/ChatSDKUI/Assets/chat_icon@3x.png differ diff --git a/ChatSDKUI/Assets/chat_icon_unSelected@2x.png b/ChatSDKUI/Assets/chat_icon_unSelected@2x.png new file mode 100644 index 00000000..ecb87d73 Binary files /dev/null and b/ChatSDKUI/Assets/chat_icon_unSelected@2x.png differ diff --git a/ChatSDKUI/Assets/chat_icon_unSelected@3x.png b/ChatSDKUI/Assets/chat_icon_unSelected@3x.png new file mode 100644 index 00000000..58429c07 Binary files /dev/null and b/ChatSDKUI/Assets/chat_icon_unSelected@3x.png differ diff --git a/ChatSDKUI/Assets/checkBox@2x.png b/ChatSDKUI/Assets/checkBox@2x.png new file mode 100755 index 00000000..2a7b7a40 Binary files /dev/null and b/ChatSDKUI/Assets/checkBox@2x.png differ diff --git a/ChatSDKUI/Assets/checkBox@3x.png b/ChatSDKUI/Assets/checkBox@3x.png new file mode 100755 index 00000000..42ab4b48 Binary files /dev/null and b/ChatSDKUI/Assets/checkBox@3x.png differ diff --git a/ChatSDKUI/Assets/checkbox_disabeled@2x.png b/ChatSDKUI/Assets/checkbox_disabeled@2x.png new file mode 100755 index 00000000..0fb6cbe5 Binary files /dev/null and b/ChatSDKUI/Assets/checkbox_disabeled@2x.png differ diff --git a/ChatSDKUI/Assets/checkbox_disabeled@3x.png b/ChatSDKUI/Assets/checkbox_disabeled@3x.png new file mode 100755 index 00000000..328f95cf Binary files /dev/null and b/ChatSDKUI/Assets/checkbox_disabeled@3x.png differ diff --git a/ChatSDKUI/Assets/empty_chat_view@2x.png b/ChatSDKUI/Assets/empty_chat_view@2x.png new file mode 100755 index 00000000..bf3949c4 Binary files /dev/null and b/ChatSDKUI/Assets/empty_chat_view@2x.png differ diff --git a/ChatSDKUI/Assets/empty_chat_view@3x.png b/ChatSDKUI/Assets/empty_chat_view@3x.png new file mode 100755 index 00000000..63f9f111 Binary files /dev/null and b/ChatSDKUI/Assets/empty_chat_view@3x.png differ diff --git a/ChatSDKUI/Assets/empty_checkBox@2x.png b/ChatSDKUI/Assets/empty_checkBox@2x.png new file mode 100755 index 00000000..8a118f82 Binary files /dev/null and b/ChatSDKUI/Assets/empty_checkBox@2x.png differ diff --git a/ChatSDKUI/Assets/empty_checkBox@3x.png b/ChatSDKUI/Assets/empty_checkBox@3x.png new file mode 100755 index 00000000..4dc5ede3 Binary files /dev/null and b/ChatSDKUI/Assets/empty_checkBox@3x.png differ diff --git a/ChatSDKUI/Classes/.gitkeep b/ChatSDKUI/Classes/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/ChatSDKUI/Classes/Actions/BSelectLocationAction.m b/ChatSDKUI/Classes/Actions/BSelectLocationAction.m index f4b7764d..ec6ac9e0 100755 --- a/ChatSDKUI/Classes/Actions/BSelectLocationAction.m +++ b/ChatSDKUI/Classes/Actions/BSelectLocationAction.m @@ -18,7 +18,7 @@ -(RXPromise *) execute { else { _promise = [RXPromise new]; } - + /* if(!_locationManager) { _locationManager = [[CLLocationManager alloc] init]; @@ -32,7 +32,7 @@ -(RXPromise *) execute { _locationManager.desiredAccuracy = kCLLocationAccuracyBest; } [_locationManager startUpdatingLocation]; - + */ return _promise; } diff --git a/ChatSDKUI/Classes/Categories/NSMutableArray+User.m b/ChatSDKUI/Classes/Categories/NSMutableArray+User.m index 29a8dbb3..807387d7 100755 --- a/ChatSDKUI/Classes/Categories/NSMutableArray+User.m +++ b/ChatSDKUI/Classes/Categories/NSMutableArray+User.m @@ -22,7 +22,7 @@ - (void) sortAlphabetical { if ([obj2 respondsToSelector:@selector(user)]) { u2 = [obj2 user]; } - return [u1.name compare:u2.name]; + return [u1.name caseInsensitiveCompare:u2.name]; }]; } diff --git a/ChatSDKUI/Classes/Components/Chat View/BChatViewController.h b/ChatSDKUI/Classes/Components/Chat View/BChatViewController.h index 823cfbbf..11966f57 100755 --- a/ChatSDKUI/Classes/Components/Chat View/BChatViewController.h +++ b/ChatSDKUI/Classes/Components/Chat View/BChatViewController.h @@ -26,5 +26,6 @@ -(instancetype) initWithThread: (id) thread; - (void) updateSubtitle; -(void) updateTitle; +//-(void) setThreadName; @end diff --git a/ChatSDKUI/Classes/Components/Chat View/BChatViewController.m b/ChatSDKUI/Classes/Components/Chat View/BChatViewController.m index e485c4eb..c4fc5dc9 100755 --- a/ChatSDKUI/Classes/Components/Chat View/BChatViewController.m +++ b/ChatSDKUI/Classes/Components/Chat View/BChatViewController.m @@ -10,7 +10,6 @@ #import #import - @implementation BChatViewController @synthesize thread = _thread; @@ -54,11 +53,13 @@ -(void) updateSubtitle { } if (_thread.type.intValue & bThreadFilterGroup) { - [self setSubtitle:_thread.memberListString]; +// [self setSubtitle:_thread.memberListString]; } else { // 1-to-1 Chat + // hide right bar button item + [self hideRightBarButton]; if (_thread.otherUser.online.boolValue) { - [self setSubtitle:[NSBundle t: bOnline]]; + [self setSubtitle:[NSBundle t: NSLocalizedString(bOnline, nil)]]; } else if(BChatSDK.lastOnline) { __weak __typeof__(self) weakSelf = self; [BChatSDK.lastOnline getLastOnlineForUser:_thread.otherUser].thenOnMain(^id(NSDate * date) { @@ -69,6 +70,54 @@ -(void) updateSubtitle { } } +-(void)leaveChat { + [BChatSDK.core deleteThread:_thread]; + [BChatSDK.core leaveThread:_thread]; + // if true, back was pressed + if (![self.navigationController.viewControllers indexOfObject:self]==NSNotFound) { + [self.navigationController popViewControllerAnimated:true]; + } +} + +-(void) openInviteScreen { + + + BFriendsListViewController * vc = [[BFriendsListViewController alloc] initWithUsersToExclude:_thread.users.allObjects onComplete:nil]; + // [[BFriendsListViewController alloc] initWithUsersToExclude:usersToExclude onComplete:action] + + UINavigationController * nav = [BChatSDK.ui friendsNavigationControllerWithUsersToExclude:_thread.users.allObjects onComplete:^(NSArray * users, NSString * groupName){ + + [BChatSDK.core addUsers:users toThread:_thread].thenOnMain(^id(id success){ + [UIView alertWithTitle:[NSBundle t:bSuccess] withMessage:[NSBundle t:bAdded]]; + // [self reloadData]; + return Nil; + }, Nil); + }]; + [((id) nav.topViewController) setRightBarButtonActionTitle:[NSBundle t: bAdd]]; + [self.navigationController pushViewController:vc animated:YES]; + + + //[self presentViewController:nav animated:YES completion:Nil]; +} + + +-(void) addUser { + // Use initWithThread here to make sure we don't show any users already in the thread + // Show the friends view controller + UINavigationController * nav = [BChatSDK.ui friendsNavigationControllerWithUsersToExclude:_thread.users.allObjects onComplete:^(NSArray * users, NSString * groupName){ + [BChatSDK.core addUsers:users toThread:_thread].thenOnMain(^id(id success){ + [UIView alertWithTitle:[NSBundle t:bSuccess] withMessage:[NSBundle t:bAdded]]; + + // [self reloadData]; + return Nil; + }, Nil); + }]; + [((id) nav.topViewController) setRightBarButtonActionTitle:[NSBundle t: bAdd]]; + [self.navigationController pushViewController:[nav topViewController] animated:YES]; + + // [self presentViewController:nav animated:YES completion:Nil]; +} + -(void) addObservers { [super addObservers]; @@ -172,6 +221,8 @@ -(void) viewDidAppear:(BOOL)animated { } + + -(void) addUserToPublicThreadIfNecessary { // For public threads we add the user when we view the thread // TODO: This is called multiple times... maybe move it to view did load @@ -184,6 +235,11 @@ -(void) addUserToPublicThreadIfNecessary { -(void) viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; + // Delete thread if no message was sent + if (!_thread.newestMessage) { + [self leaveChat]; + } + // Remove the user from the thread if (_thread.type.intValue & bThreadFilterPublic && !_usersViewLoaded) { id currentUser = BChatSDK.currentUser; @@ -224,6 +280,11 @@ -(void) markRead { } } +//-(void) setThreadName { +// //[_thread setMetaValue:@"Test" forKey:@"name"]; +// +//} + -(bThreadType) threadType { return _thread.type.intValue; } @@ -234,9 +295,10 @@ -(void) navigationBarTapped { [users removeObject:BChatSDK.currentUser]; UINavigationController * nvc = [BChatSDK.ui usersViewNavigationControllerWithThread:_thread - parentNavigationController:self.navigationController]; - - [self presentViewController:nvc animated:YES completion:nil]; + parentNavigationController:self.navigationController]; + [self.navigationController pushViewController:[nvc topViewController] animated:YES]; + + // [self presentViewController:nvc animated:YES completion:nil]; } diff --git a/ChatSDKUI/Classes/Components/Chat View/BTextInputView.h b/ChatSDKUI/Classes/Components/Chat View/BTextInputView.h index c3286f74..115b2fa1 100755 --- a/ChatSDKUI/Classes/Components/Chat View/BTextInputView.h +++ b/ChatSDKUI/Classes/Components/Chat View/BTextInputView.h @@ -31,7 +31,6 @@ } @property (weak, nonatomic, readwrite) id sendBarDelegate; - // This is a property so we can access it from our mentions view @property (nonatomic, readwrite) HKWTextView * textView; diff --git a/ChatSDKUI/Classes/Components/Chat View/BTextInputView.m b/ChatSDKUI/Classes/Components/Chat View/BTextInputView.m index ea2a4a4c..f06dc53d 100755 --- a/ChatSDKUI/Classes/Components/Chat View/BTextInputView.m +++ b/ChatSDKUI/Classes/Components/Chat View/BTextInputView.m @@ -16,7 +16,7 @@ #define bTextViewVerticalPadding 5.72 #define bFontSize 19 -#define bMaxLines 5 +#define bMaxLines 5 // this will not work as shouldChangeTextInRange implementation is changed #define bMinLines 1 #define bMaxCharacters 0 @@ -29,12 +29,17 @@ @implementation BTextInputView @synthesize sendButton = _sendButton; @synthesize placeholderLabel = _placeholderLabel; + +- (double)extracted { + return bMargin; +} + -(instancetype) initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // self.barTintColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.7]; - self.backgroundColor = [UIColor whiteColor]; + self.backgroundColor = [UIColor colorWithRed:242/255.0f green:242/255.0f blue:242/255.0f alpha:1.0];// [UIColor redColor];// // Decide how many lines the message should have minLines = bMinLines; @@ -47,17 +52,23 @@ -(instancetype) initWithFrame:(CGRect)frame { // Create an options button which shows an action sheet _optionsButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _optionsButton.hidden = YES; [self addSubview:_optionsButton]; _textView = [[HKWTextView alloc] init]; // If we use the mentions functionality we need to set the external delegate // This is the way to set the UITextView delegate to keep mentions functionality working _textView.simpleDelegate = self; - + _textView.layer.cornerRadius = 2; + _textView.layer.borderColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1.0].CGColor; + _textView.layer.borderWidth = 1.0; + _textView.layer.masksToBounds = true; + [_textView setClipsToBounds:YES]; [self addSubview: _textView]; // Add a send button _sendButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; + [[_sendButton titleLabel] setFont:[UIFont fontWithName:@"SFProText-Regular" size:bDefaultFontSize]]; [self addSubview: _sendButton]; [_optionsButton setImage:[NSBundle uiImageNamed:@"icn_24_options.png"] forState:UIControlStateNormal]; @@ -75,19 +86,20 @@ -(instancetype) initWithFrame:(CGRect)frame { [_sendButton addTarget:self action:@selector(sendButtonCancelled) forControlEvents:UIControlEventTouchUpOutside]; _textView.scrollEnabled = YES; - _textView.backgroundColor = [UIColor clearColor]; - + _textView.backgroundColor = [UIColor whiteColor]; + _textView.font = [UIFont fontWithName:@"SFProText-Regular" size:bDefaultFontSize]; // For some reason using scrollEnabled = NO causes probalems _textView.bounces = NO; // Adjust the insets to make the text closer to the outside of the // box - ios6 is slightly different from ios7 - if ([UIDevice currentDevice].systemVersion.intValue < 7) { - _textView.contentInset = UIEdgeInsetsMake(-6.0, -4.0, -6.0, 0.0); - } - else { - _textView.contentInset = UIEdgeInsetsMake(-6.0, -1.0, -6.0, 0.0); - } +// if ([UIDevice currentDevice].systemVersion.intValue < 7) { +// _textView.contentInset = UIEdgeInsetsMake(-6.0, -4.0, -6.0, 0.0); +// } +// else { +// _textView.contentInset = UIEdgeInsetsMake(-6.0, 5.0, -6.0, 0.0); +// // _textView.contentInset = UIEdgeInsetsMake(-6.0, -1.0, -6.0, 0.0); +// } // Constrain the elements _optionsButton.keepLeftInset.equal = bMargin +keepRequired; @@ -101,40 +113,43 @@ -(instancetype) initWithFrame:(CGRect)frame { _optionsButton.translatesAutoresizingMaskIntoConstraints = NO; _sendButton.keepRightInset.equal = bMargin; - _sendButton.keepBottomInset.equal = 0; + _sendButton.keepVerticalCenter.equal = 0.5; + // _sendButton.keepBottomInset.equal = 5; + //_sendButton.keepHorizontalAlignTo(_textView); + //_sendButton.keepCenterAlignTo(_textView); + _sendButton.keepHeight.equal = 40; _sendButton.keepWidth.equal = 48; - _sendButton.translatesAutoresizingMaskIntoConstraints = NO; - - _textView.keepLeftOffsetTo(_optionsButton).equal = bMargin; + + _textView.keepLeftOffsetTo(_optionsButton).equal = [self extracted]; _textView.keepRightOffsetTo(_sendButton).equal = bMargin; _textView.keepBottomInset.equal = bMargin; _textView.keepTopInset.equal = bMargin; _textView.translatesAutoresizingMaskIntoConstraints = NO; + //_textView.text = [NSBundle t:NSLocalizedString(bWriteSomething, nil)]; // Create a placeholder text label _placeholderLabel = [[UILabel alloc] init]; [self addSubview:_placeholderLabel]; - - _placeholderLabel.keepBottomInset.equal = 0; - _placeholderLabel.keepTopInset.equal = 0; - _placeholderLabel.keepLeftOffsetTo(_optionsButton).equal = bMargin + 4; + + _placeholderLabel.keepBottomInset.equal = bMargin;//0; + _placeholderLabel.keepTopInset.equal = bMargin;//0; + _placeholderLabel.keepLeftOffsetTo(_optionsButton).equal = bMargin + 4; //bMargin + 14; _placeholderLabel.keepWidth.equal = 200; [_placeholderLabel setBackgroundColor:[UIColor clearColor]]; - + [_placeholderLabel setTextColor:_placeholderColor]; + [_placeholderLabel setText:[NSBundle t:NSLocalizedString(bWriteSomething, nil)]]; - [_placeholderLabel setText:[NSBundle t:bWriteSomething]]; - - [self setFont:[UIFont systemFontOfSize:bFontSize]]; - + [self setFont:[UIFont fontWithName:@"SFProText-Regular" size:bDefaultFontSize]]; + __weak __typeof__(self) weakSelf = self; _internetConnectionHook = [BHook hook:^(NSDictionary * data) { __typeof__(self) strongSelf = weakSelf; [strongSelf updateInterfaceForReachabilityStateChange]; }]; [BChatSDK.hook addHook:_internetConnectionHook withName:bHookInternetConnectivityDidChange]; - + [self updateInterfaceForReachabilityStateChange]; UIView * topMarginView = [[UIView alloc] initWithFrame:CGRectZero]; @@ -169,16 +184,22 @@ -(void) setMicButtonEnabled: (BOOL) enabled { -(void) setMicButtonEnabled: (BOOL) enabled sendButtonEnabled: (BOOL) sendButtonEnabled { _micButtonEnabled = enabled; - _sendButton.enabled = sendButtonEnabled || enabled; + _sendButton.enabled = sendButtonEnabled; if (enabled) { [_sendButton setTitle:Nil forState:UIControlStateNormal]; [_sendButton setImage:[NSBundle uiImageNamed: @"icn_24_mic.png"] forState:UIControlStateNormal]; } else { - [_sendButton setTitle:[NSBundle t:bSend] forState:UIControlStateNormal]; + [_sendButton setTitle:[NSBundle t:NSLocalizedString(bSend, nil)] forState:UIControlStateNormal]; [_sendButton setImage:Nil forState:UIControlStateNormal]; } + if (_sendButton.enabled){ + [_sendButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + } + else{ + [_sendButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal]; + } } #pragma Button Delegates @@ -203,7 +224,6 @@ -(void) sendButtonPressed { NSString * newMessage = [_textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; [BChatSDK.core sendMessageWithText:newMessage withThreadEntityID:_sendBarDelegate.threadEntityID]; } - _textView.text = @""; [self textViewDidChange:_textView]; } @@ -242,8 +262,8 @@ -(void) sendAudioMessage { // TODO: Make the tost position above the text bar programatically UIView * view = _sendBarDelegate.viewController.view; [view makeToast:[NSBundle t:bHoldToSendAudioMessageError] - duration:2 - position:[NSValue valueWithCGPoint: CGPointMake(view.frame.size.width / 2.0, view.frame.size.height - 120)]]; + duration:2 + position:[NSValue valueWithCGPoint: CGPointMake(view.frame.size.width / 2.0, view.frame.size.height - 120)]]; } } @@ -269,7 +289,7 @@ -(void) showRecordingToast { -(void) stopRecording { [[BAudioManager sharedManager] finishRecording]; [_sendBarDelegate.view hideAllToasts]; - [_placeholderLabel setText:[NSBundle t:bWriteSomething]]; + [_placeholderLabel setText:[NSBundle t:NSLocalizedString(bWriteSomething, nil)]]; [self cancelRecordingToastTimer]; } @@ -304,7 +324,7 @@ -(void) cancelRecordingToastTimer { // If the user touches up off the button we cancel the recording - (void)sendButtonCancelled { [_sendBarDelegate.view hideAllToasts]; - [_placeholderLabel setText:[NSBundle t:bWriteSomething]]; + [_placeholderLabel setText:[NSBundle t:NSLocalizedString(bWriteSomething, nil)]]; CSToastStyle * style = [[CSToastStyle alloc] initWithDefaultStyle]; style.backgroundColor = [UIColor redColor]; [_sendBarDelegate.view makeToast:[NSBundle t:bCancelled] @@ -365,6 +385,14 @@ -(float) getTextHeight: (NSString *) text { context:Nil].size.height; } + +- (BOOL)textViewShouldEndEditing:(UITextView *)textView { + _placeholderLabel.hidden = ![textView.text isEqualToString:@""]; + return true; +} + + + -(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { // Typing Indicator @@ -377,16 +405,17 @@ -(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range re if(maxCharacters > 0 && newText.length > maxCharacters) { return NO; - } - - NSInteger numberOfLines = [self getTextHeight:newText]/textView.font.lineHeight; - numberOfLines = MAX(numberOfLines, [newText componentsSeparatedByString:@"\n"].count); - - if (numberOfLines > maxLines) { - return NO; } - else return YES; + // NSInteger numberOfLines = [self getTextHeight:newText]/textView.font.lineHeight; + // numberOfLines = MAX(numberOfLines, [newText componentsSeparatedByString:@"\n"].count); + + // if (numberOfLines > maxLines) { + // return NO; + // } + // + // else + return YES; } -(void) textViewDidChange:(UITextView *)textView { @@ -398,7 +427,7 @@ -(void) textViewDidChange:(UITextView *)textView { else { [self setMicButtonEnabled:YES]; } - + [self resizeToolbar]; @@ -408,18 +437,38 @@ -(void) textViewDidChange:(UITextView *)textView { -(void) resizeToolbar { + float originalHeight = self.keepHeight.equal; // float newHeight = MAX(_textView.contentSize.height, _textView.font.lineHeight);//[self getTextBoxTextHeight]; + + // float newHeight = MAX(_textView.font.lineHeight, [self measureHeightOfUITextView:_textView]); float newHeight = MAX(_textView.font.lineHeight, [self measureHeightOfUITextView:_textView]); - + // _textView.text = [NSBundle t:NSLocalizedString(bWriteSomething, nil)]; + +// if ([_textView.text isEqualToString: [NSBundle t:NSLocalizedString(bWriteSomething, nil)]]) +// { +// newHeight = _textView.font.lineHeight; +// } // Calcualte the new textview height float textBoxHeight = newHeight + bTextViewVerticalPadding; - + // Make textview scrollable + CGRect screenRect = [[UIScreen mainScreen] bounds]; + CGFloat screenHeight = screenRect.size.height; + CGFloat textViewCurrentHeight = self.keepHeight.equal; + if (textViewCurrentHeight > screenHeight/6) { + return; + } // Set the toolbar height - the text view will resize automatically // using autolayout - self.keepHeight.equal = bMargin * 2 + textBoxHeight; - + float newTextBoxHeight = (bMargin * 2 + textBoxHeight); + if (newTextBoxHeight > screenHeight/6) { + self.keepHeight.equal = screenHeight/6; + } + else { + self.keepHeight.equal = newTextBoxHeight; + } + float delta = self.keepHeight.equal - originalHeight; if(fabsf(delta) > 0.01) { @@ -439,6 +488,7 @@ -(void) resizeToolbar { } }]; } + } - (CGFloat)measureHeightOfUITextView:(UITextView *)textView @@ -473,13 +523,17 @@ - (CGFloat)measureHeightOfUITextView:(UITextView *)textView // NSString class method: boundingRectWithSize:options:attributes:context is // available only on ios7.0 sdk. - +// CGSize textViewSize = [text sizeWithFont:[UIFont fontWithName:@"Marker Felt" size:20] +// constrainedToSize:CGSizeMake(WIDHT_OF_VIEW, FLT_MAX) +// lineBreakMode:UILineBreakModeTailTruncation]; NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; - [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping]; + [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping]; //NSLineBreakByWordWrapping NSDictionary *attributes = @{ NSFontAttributeName: textView.font, NSParagraphStyleAttributeName : paragraphStyle }; - CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT) + + CGRect size = + [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil]; diff --git a/ChatSDKUI/Classes/Components/Chat View/BTextInputView.m.orig b/ChatSDKUI/Classes/Components/Chat View/BTextInputView.m.orig new file mode 100755 index 00000000..82f40551 --- /dev/null +++ b/ChatSDKUI/Classes/Components/Chat View/BTextInputView.m.orig @@ -0,0 +1,575 @@ +// +// BTextInputView.m +// Chat SDK +// +// Created by Benjamin Smiley-andrews on 25/09/2013. +// Copyright (c) 2013 deluge. All rights reserved. +// + +#import "BTextInputView.h" +#import + +#define bMargin 8.0 + +// The amount of padding (above + below) the text +// i.e. textView height = text height + padding +#define bTextViewVerticalPadding 5.72 + +#define bFontSize 19 +#define bMaxLines 5 // this will not work as shouldChangeTextInRange implementation is changed +#define bMinLines 1 +#define bMaxCharacters 0 + +@implementation BTextInputView + +@synthesize textView = _textView; +@synthesize maxLines, minLines, maxCharacters; +@synthesize sendBarDelegate = _sendBarDelegate; +@synthesize optionsButton = _optionsButton; +@synthesize sendButton = _sendButton; +@synthesize placeholderLabel = _placeholderLabel; + + +- (double)extracted { + return bMargin; +} + +-(instancetype) initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + +// self.barTintColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.7]; + self.backgroundColor = [UIColor colorWithRed:242/255.0f green:242/255.0f blue:242/255.0f alpha:1.0];// [UIColor redColor];// + + // Decide how many lines the message should have + minLines = bMinLines; + maxLines = bMaxLines; + maxCharacters = bMaxCharacters; + + // Set the text color + _placeholderColor = [UIColor darkGrayColor]; + _textColor = [UIColor blackColor]; + + // Create an options button which shows an action sheet + _optionsButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _optionsButton.hidden = YES; + [self addSubview:_optionsButton]; + + _textView = [[HKWTextView alloc] init]; + // If we use the mentions functionality we need to set the external delegate + // This is the way to set the UITextView delegate to keep mentions functionality working + _textView.simpleDelegate = self; + _textView.layer.cornerRadius = 2; + _textView.layer.borderColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1.0].CGColor; + _textView.layer.borderWidth = 1.0; + _textView.layer.masksToBounds = true; + [_textView setClipsToBounds:YES]; + [self addSubview: _textView]; + + // Add a send button + _sendButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; + [[_sendButton titleLabel] setFont:[UIFont fontWithName:@"SFProText-Regular" size:bDefaultFontSize]]; + [self addSubview: _sendButton]; + + [_optionsButton setImage:[NSBundle uiImageNamed:@"icn_24_options.png"] forState:UIControlStateNormal]; + [_optionsButton setImage:[NSBundle uiImageNamed:@"icn_24_keyboard.png"] forState:UIControlStateSelected]; + + [_optionsButton addTarget:self action:@selector(optionsButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + + NSString * sendButtonTitle = [NSBundle t:bSend]; + [_sendButton setTitle:sendButtonTitle forState:UIControlStateNormal]; + + [_sendButton addTarget:self action:@selector(sendButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_sendButton addTarget:self action:@selector(sendButtonHeld) forControlEvents:UIControlEventTouchDown]; + + // We don't want to send a message if they touch up outside the button area + [_sendButton addTarget:self action:@selector(sendButtonCancelled) forControlEvents:UIControlEventTouchUpOutside]; + + _textView.scrollEnabled = YES; + _textView.backgroundColor = [UIColor whiteColor]; + _textView.font = [UIFont fontWithName:@"SFProText-Regular" size:bDefaultFontSize]; + // For some reason using scrollEnabled = NO causes probalems + _textView.bounces = NO; + + // Adjust the insets to make the text closer to the outside of the + // box - ios6 is slightly different from ios7 +// if ([UIDevice currentDevice].systemVersion.intValue < 7) { +// _textView.contentInset = UIEdgeInsetsMake(-6.0, -4.0, -6.0, 0.0); +// } +// else { +// _textView.contentInset = UIEdgeInsetsMake(-6.0, 5.0, -6.0, 0.0); +// // _textView.contentInset = UIEdgeInsetsMake(-6.0, -1.0, -6.0, 0.0); +// } + + // Constrain the elements + _optionsButton.keepLeftInset.equal = bMargin +keepRequired; + + _optionsButton.keepBottomInset.equal = 8.0; + _optionsButton.keepHeight.equal = 24; + + // If the user has no chat options available then remove the chat option button width + _optionsButton.keepWidth.equal = BChatSDK.ui.chatOptions.count ? 24 : 0; + + _optionsButton.translatesAutoresizingMaskIntoConstraints = NO; + + _sendButton.keepRightInset.equal = bMargin; + _sendButton.keepVerticalCenter.equal = 0.5; + // _sendButton.keepBottomInset.equal = 5; + //_sendButton.keepHorizontalAlignTo(_textView); + //_sendButton.keepCenterAlignTo(_textView); + + _sendButton.keepHeight.equal = 40; + _sendButton.keepWidth.equal = 48; +<<<<<<< HEAD + + _textView.keepLeftOffsetTo(_optionsButton).equal = [self extracted]; +======= + _sendButton.translatesAutoresizingMaskIntoConstraints = NO; + + _textView.keepLeftOffsetTo(_optionsButton).equal = bMargin; +>>>>>>> ba622d8b140a9791dcf57da71313e8792eb58a33 + _textView.keepRightOffsetTo(_sendButton).equal = bMargin; + _textView.keepBottomInset.equal = bMargin; + _textView.keepTopInset.equal = bMargin; + _textView.translatesAutoresizingMaskIntoConstraints = NO; + //_textView.text = [NSBundle t:NSLocalizedString(bWriteSomething, nil)]; + + // Create a placeholder text label + _placeholderLabel = [[UILabel alloc] init]; + [self addSubview:_placeholderLabel]; + + _placeholderLabel.keepBottomInset.equal = bMargin;//0; + _placeholderLabel.keepTopInset.equal = bMargin;//0; + _placeholderLabel.keepLeftOffsetTo(_optionsButton).equal = bMargin + 4; //bMargin + 14; + _placeholderLabel.keepWidth.equal = 200; + [_placeholderLabel setBackgroundColor:[UIColor clearColor]]; + + [_placeholderLabel setTextColor:_placeholderColor]; + [_placeholderLabel setText:[NSBundle t:NSLocalizedString(bWriteSomething, nil)]]; + + [self setFont:[UIFont fontWithName:@"SFProText-Regular" size:bDefaultFontSize]]; + + __weak __typeof__(self) weakSelf = self; + _internetConnectionHook = [BHook hook:^(NSDictionary * data) { + __typeof__(self) strongSelf = weakSelf; + [strongSelf updateInterfaceForReachabilityStateChange]; + }]; + [BChatSDK.hook addHook:_internetConnectionHook withName:bHookInternetConnectivityDidChange]; + + [self updateInterfaceForReachabilityStateChange]; + + UIView * topMarginView = [[UIView alloc] initWithFrame:CGRectZero]; + topMarginView.backgroundColor = [UIColor lightGrayColor]; + + [self addSubview:topMarginView]; + + topMarginView.keepTopInset.equal = 0 + keepRequired; + topMarginView.keepLeftInset.equal = 0 + keepRequired; + topMarginView.keepRightInset.equal = 0 + keepRequired; + topMarginView.keepHeight.equal = 0.5 + keepRequired; + + [self resizeToolbar]; + } + return self; +} + +-(void) updateInterfaceForReachabilityStateChange { + BOOL connected = BChatSDK.connectivity.isConnected; + _sendButton.enabled = connected; + _optionsButton.enabled = connected; +} + +-(void) setAudioEnabled: (BOOL) audioEnabled { + _audioEnabled = audioEnabled; + [self setMicButtonEnabled:_audioEnabled]; +} + +-(void) setMicButtonEnabled: (BOOL) enabled { + [self setMicButtonEnabled:enabled sendButtonEnabled:NO]; +} + +-(void) setMicButtonEnabled: (BOOL) enabled sendButtonEnabled: (BOOL) sendButtonEnabled { + _micButtonEnabled = enabled; + _sendButton.enabled = sendButtonEnabled; + if (enabled) { + [_sendButton setTitle:Nil forState:UIControlStateNormal]; + [_sendButton setImage:[NSBundle uiImageNamed: @"icn_24_mic.png"] + forState:UIControlStateNormal]; + } + else { + [_sendButton setTitle:[NSBundle t:NSLocalizedString(bSend, nil)] forState:UIControlStateNormal]; + [_sendButton setImage:Nil forState:UIControlStateNormal]; + } + if (_sendButton.enabled){ + [_sendButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + } + else{ + [_sendButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal]; + } +} + +#pragma Button Delegates + +-(void) sendButtonPressed { + + if (_audioMaxLengthReached) { + _audioMaxLengthReached = NO; + return; + } + + [self stopRecording]; + + if (!_micButtonEnabled) { + + // Check if the message is empty + if ([[_textView.text stringByReplacingOccurrencesOfString:@" " withString:@""] isEqualToString:@""]) { + return; + } + + if (_sendBarDelegate && [_sendBarDelegate respondsToSelector:@selector(threadEntityID)]) { + NSString * newMessage = [_textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + [BChatSDK.core sendMessageWithText:newMessage withThreadEntityID:_sendBarDelegate.threadEntityID]; + } + _textView.text = @""; + [self textViewDidChange:_textView]; + } + else { + [self sendAudioMessage]; + } +} + +// When the button is held we start recording +- (void)sendButtonHeld { + if (_micButtonEnabled) { + [[BAudioManager sharedManager] startRecording]; + _recordingToastTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 + target:self + selector:@selector(showRecordingToast) + userInfo:Nil + repeats:YES]; + _recordingStart = [NSDate new]; + [_placeholderLabel setText:[NSBundle t: bSlideToCancel]]; + } +} + +-(void) sendAudioMessage { + // This is where the button is released so we want to finish recording and send + if (_sendBarDelegate) { + + // Return the recording url and duration in an array + NSURL * audioURL = [BAudioManager sharedManager].recorder.url; + NSData * audioData = [NSData dataWithContentsOfURL:audioURL]; + double duration = [BAudioManager sharedManager].recordingLength; + + if (duration > 1) { + [BChatSDK.audioMessage sendMessageWithAudio:audioData duration:duration withThreadEntityID:_sendBarDelegate.threadEntityID]; + } + else { + // TODO: Make the tost position above the text bar programatically + UIView * view = _sendBarDelegate.viewController.view; + [view makeToast:[NSBundle t:bHoldToSendAudioMessageError] + duration:2 + position:[NSValue valueWithCGPoint: CGPointMake(view.frame.size.width / 2.0, view.frame.size.height - 120)]]; + + } + } +} + +-(void) showRecordingToast { + NSString * text = [NSBundle t:bRecording]; + + int remainingTime = BChatSDK.config.audioMessageMaxLengthSeconds + [_recordingStart timeIntervalSinceNow]; + if (remainingTime <= 10) { + text = [NSString stringWithFormat:[NSBundle t: bSecondsRemaining_], remainingTime]; + } + if (remainingTime <= 0) { + _audioMaxLengthReached = YES; + [self stopRecording]; + [self presentAlertView]; + } + [_sendBarDelegate.view makeToast:text + duration:0.7 + position:[NSValue valueWithCGPoint: CGPointMake(_sendBarDelegate.view.frame.size.width / 2.0, self.frame.origin.y - self.frame.size.height - 20)]]; +} + +-(void) stopRecording { + [[BAudioManager sharedManager] finishRecording]; + [_sendBarDelegate.view hideAllToasts]; + [_placeholderLabel setText:[NSBundle t:NSLocalizedString(bWriteSomething, nil)]]; + [self cancelRecordingToastTimer]; +} + +-(void) presentAlertView { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSBundle t:bAudioLengthLimitReached] + message:[NSBundle t:bSendOrDiscardRecording] + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *submit = [UIAlertAction actionWithTitle:[NSBundle t:bSend] style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + [self sendAudioMessage]; + }]; + + UIAlertAction * cancel = [UIAlertAction actionWithTitle:[NSBundle t:bCancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { + }]; + + [alert addAction:submit]; + [alert addAction:cancel]; + + [self.sendBarDelegate.viewController presentViewController:alert animated:YES completion:Nil]; + +} + +-(void) cancelRecordingToastTimer { + if(_recordingToastTimer) { + [_recordingToastTimer invalidate]; + _recordingToastTimer = Nil; + _recordingStart = Nil; + } +} + +// If the user touches up off the button we cancel the recording +- (void)sendButtonCancelled { + [_sendBarDelegate.view hideAllToasts]; + [_placeholderLabel setText:[NSBundle t:NSLocalizedString(bWriteSomething, nil)]]; + CSToastStyle * style = [[CSToastStyle alloc] initWithDefaultStyle]; + style.backgroundColor = [UIColor redColor]; + [_sendBarDelegate.view makeToast:[NSBundle t:bCancelled] + duration:1 + position:[NSValue valueWithCGPoint: CGPointMake(_sendBarDelegate.view.frame.size.width / 2.0, self.frame.origin.y - self.frame.size.height - 20)] style:style]; + [self cancelRecordingToastTimer]; + [[BAudioManager sharedManager] finishRecording]; +} + +-(void) optionsButtonPressed { + if (_optionsButton.selected) { + [self hideOptions]; + } + else { + [self showOptions]; + } +} + +-(void) showOptions { + if (_sendBarDelegate && [_sendBarDelegate respondsToSelector:@selector(showOptions)]) { + if([_sendBarDelegate showOptions]) { + _optionsButton.selected = YES; + } + } +} + +-(void) hideOptions { + if (_sendBarDelegate && [_sendBarDelegate respondsToSelector:@selector(showOptions)]) { + if([_sendBarDelegate hideOptions]) { + _optionsButton.selected = NO; + } + } +} + +-(void) setFont: (UIFont *) font { + [_textView setFont:font]; + [_placeholderLabel setFont:font]; + [self resizeToolbar]; +} + +-(float) getTextBoxTextHeight { + NSString * text = _textView.text; + + + // If it ends in a new line this isn't included in the size so add an extra character + if ([text hasSuffix:@"\n"] || [text isEqualToString:@""]) { + text = [text stringByAppendingString:@"-"]; + } + + return [self getTextHeight:text]; +} + +-(float) getTextHeight: (NSString *) text { + NSString * nonBlankText = [text stringByReplacingOccurrencesOfString:@" " withString:@"_"]; + return [nonBlankText boundingRectWithSize:CGSizeMake(_textView.contentSize.width - 1, CGFLOAT_MAX) + options:NSStringDrawingUsesLineFragmentOrigin + attributes:@{NSFontAttributeName: _textView.font} + context:Nil].size.height; +} + + +- (BOOL)textViewShouldEndEditing:(UITextView *)textView { + _placeholderLabel.hidden = ![textView.text isEqualToString:@""]; + return true; +} + + + +-(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + + // Typing Indicator + if (_sendBarDelegate && [_sendBarDelegate respondsToSelector:@selector(typing)]) { + [_sendBarDelegate typing]; + } + + // Workout if adding this text will cause the box to become too long + NSString * newText = [textView.text stringByAppendingString:text]; + + if(maxCharacters > 0 && newText.length > maxCharacters) { + return NO; + } + + // NSInteger numberOfLines = [self getTextHeight:newText]/textView.font.lineHeight; + // numberOfLines = MAX(numberOfLines, [newText componentsSeparatedByString:@"\n"].count); + + // if (numberOfLines > maxLines) { + // return NO; + // } + // + // else + return YES; +} + +-(void) textViewDidChange:(UITextView *)textView { + + // If there is text or if the audio is turned off + if (textView.text.length || !_audioEnabled) { + [self setMicButtonEnabled:NO sendButtonEnabled:textView.text.length]; + } + else { + [self setMicButtonEnabled:YES]; + } + + + [self resizeToolbar]; + + // If the text area is empty show the placeholder + _placeholderLabel.hidden = ![textView.text isEqualToString:@""]; +} + +-(void) resizeToolbar { + + + float originalHeight = self.keepHeight.equal; + +// float newHeight = MAX(_textView.contentSize.height, _textView.font.lineHeight);//[self getTextBoxTextHeight]; + + // float newHeight = MAX(_textView.font.lineHeight, [self measureHeightOfUITextView:_textView]); + float newHeight = MAX(_textView.font.lineHeight, [self measureHeightOfUITextView:_textView]); + // _textView.text = [NSBundle t:NSLocalizedString(bWriteSomething, nil)]; + +// if ([_textView.text isEqualToString: [NSBundle t:NSLocalizedString(bWriteSomething, nil)]]) +// { +// newHeight = _textView.font.lineHeight; +// } + // Calcualte the new textview height + float textBoxHeight = newHeight + bTextViewVerticalPadding; + // Make textview scrollable + CGRect screenRect = [[UIScreen mainScreen] bounds]; + CGFloat screenHeight = screenRect.size.height; + CGFloat textViewCurrentHeight = self.keepHeight.equal; + if (textViewCurrentHeight > screenHeight/6) { + return; + } + // Set the toolbar height - the text view will resize automatically + // using autolayout + float newTextBoxHeight = (bMargin * 2 + textBoxHeight); + if (newTextBoxHeight > screenHeight/6) { + self.keepHeight.equal = screenHeight/6; + } + else { + self.keepHeight.equal = newTextBoxHeight; + } + + float delta = self.keepHeight.equal - originalHeight; + + if(fabsf(delta) > 0.01) { + [self.superview setNeedsUpdateConstraints]; + + __weak __typeof__(self) weakSelf = self; + + [UIView animateWithDuration:0.2 animations:^{ + __typeof__(self) strongSelf = weakSelf; + + //[self setNeedsUpdateConstraints]; + [strongSelf.superview layoutIfNeeded]; + [strongSelf->_textView setContentOffset:CGPointZero animated:NO]; + + if(strongSelf->_sendBarDelegate != Nil) { + [strongSelf->_sendBarDelegate didResizeTextInputViewWithDelta:delta]; + } + }]; + } + +} + +- (CGFloat)measureHeightOfUITextView:(UITextView *)textView +{ + if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1) + { + // This is the code for iOS 7. contentSize no longer returns the correct value, so + // we have to calculate it. + // + // This is partly borrowed from HPGrowingTextView, but I've replaced the + // magic fudge factors with the calculated values (having worked out where + // they came from) + + CGRect frame = textView.bounds; + + // Take account of the padding added around the text. + + UIEdgeInsets textContainerInsets = textView.textContainerInset; + UIEdgeInsets contentInsets = textView.contentInset; + + CGFloat leftRightPadding = textContainerInsets.left + textContainerInsets.right + textView.textContainer.lineFragmentPadding * 2 + contentInsets.left + contentInsets.right; + CGFloat topBottomPadding = textContainerInsets.top + textContainerInsets.bottom + contentInsets.top + contentInsets.bottom; + + frame.size.width -= leftRightPadding; + frame.size.height -= topBottomPadding; + + NSString *textToMeasure = textView.text; + if ([textToMeasure hasSuffix:@"\n"]) + { + textToMeasure = [NSString stringWithFormat:@"%@-", textView.text]; + } + + // NSString class method: boundingRectWithSize:options:attributes:context is + // available only on ios7.0 sdk. +// CGSize textViewSize = [text sizeWithFont:[UIFont fontWithName:@"Marker Felt" size:20] +// constrainedToSize:CGSizeMake(WIDHT_OF_VIEW, FLT_MAX) +// lineBreakMode:UILineBreakModeTailTruncation]; + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping]; //NSLineBreakByWordWrapping + + NSDictionary *attributes = @{ NSFontAttributeName: textView.font, NSParagraphStyleAttributeName : paragraphStyle }; + + + CGRect size = + [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT) + options:NSStringDrawingUsesLineFragmentOrigin + attributes:attributes + context:nil]; + + CGFloat measuredHeight = ceilf(CGRectGetHeight(size) + topBottomPadding); + return measuredHeight; + } + else + { + return textView.contentSize.height; + } +} + +-(void) setOptionsButtonHidden: (BOOL) hidden { + _optionsButton.keepWidth.equal = hidden ? 0 : 24; +} + +-(BOOL) resignTextViewFirstResponder { +// [super resignFirstResponder]; + [self hideOptions]; + return [_textView resignFirstResponder]; +} + +-(void) becomeTextViewFirstResponder { +// [super becomeFirstResponder]; + [_textView becomeFirstResponder]; +} + +-(void) setSendBarDelegate:(id)delegate { + _sendBarDelegate = delegate; +} + +@end diff --git a/ChatSDKUI/Classes/Components/Message Cells/BMessageCell.m b/ChatSDKUI/Classes/Components/Message Cells/BMessageCell.m index 40c2e565..c65621f6 100755 --- a/ChatSDKUI/Classes/Components/Message Cells/BMessageCell.m +++ b/ChatSDKUI/Classes/Components/Message Cells/BMessageCell.m @@ -56,7 +56,9 @@ -(instancetype) initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr _nameLabel = [[UILabel alloc] initWithFrame:CGRectMake(bTimeLabelPadding, 0, 0, 0)]; _nameLabel.userInteractionEnabled = NO; - + if(BChatSDK.config.threadNameColor) { + _nameLabel.textColor = [BCoreUtilities colorWithHexString:BChatSDK.config.threadNameColor]; + } _nameLabel.font = [UIFont boldSystemFontOfSize:bDefaultUserNameLabelSize]; if(BChatSDK.config.messageNameFont) { _nameLabel.font = BChatSDK.config.messageNameFont; @@ -206,7 +208,7 @@ -(void) willDisplayCell { self.bubbleWidth, self.bubbleHeight)]; - [_nameLabel setViewFrameY:self.bubbleHeight + 5]; + // [_nameLabel setViewFrameY:self.bubbleHeight + 5]; // #1 Because of the text view insets we want the cellContentView of the // text cell to extend to the right edge of the bubble @@ -251,10 +253,13 @@ -(void) layoutSubviews { // Layout the date label this will be the full size of the cell // This will automatically center the text in the y direction // we'll set the side using text alignment - [_timeLabel setViewFrameWidth:self.fw - bTimeLabelPadding * 2.0]; - + [_timeLabel setViewFrameWidth:self.fw - bTimeLabelPadding * 2.0 - _profilePicture.fw]; + [_timeLabel setViewFrameY:bubbleImageView.fh]; + [_timeLabel setViewFrameX:self.bubbleMargin.left + _profilePicture.fx + _profilePicture.fw + xMargin + bTimeLabelPadding]; + //[_timeLabel setViewFrameX:self.bubbleMargin.left + bubbleImageView.fw + _profilePicture.fw + xMargin + bTimeLabelPadding]; // + _profilePicture.fw // We don't want the label getting in the way of the read receipt - [_timeLabel setViewFrameHeight:self.cellHeight * 0.8]; + [_timeLabel setViewFrameHeight:16]; + // [_timeLabel setViewFrameHeight:self.cellHeight * 0.8]; [_readMessageImageView setViewFrameWidth:bReadReceiptWidth]; [_readMessageImageView setViewFrameHeight:bReadReceiptHeight]; @@ -264,31 +269,42 @@ -(void) layoutSubviews { [_nameLabel setViewFrameWidth:self.fw - bTimeLabelPadding * 2.0 - _profilePicture.fw]; [_nameLabel setViewFrameHeight:self.nameHeight]; + //CHANGE // Layout the bubble // The bubble is translated the "margin" to the right of the profile picture if (!isMine) { + _profilePicture.hidden = NO; [_profilePicture setViewFrameX:_profilePicture.hidden ? 0 : self.profilePicturePadding]; + [_profilePicture setViewFrameY: _nameLabel.fh]; [bubbleImageView setViewFrameX:self.bubbleMargin.left + _profilePicture.fx + _profilePicture.fw + xMargin]; - [_nameLabel setViewFrameX:bTimeLabelPadding]; + [bubbleImageView setViewFrameY:_nameLabel.fh]; + // [_timeLabel setViewFrameY:_profilePicture.fh + _nameLabel.fh]; + [_timeLabel setViewFrameY:bubbleImageView.fh + bubbleImageView.fy]; - _timeLabel.textAlignment = NSTextAlignmentRight; + [_nameLabel setViewFrameX:_profilePicture.fw + bTimeLabelPadding]; + [_nameLabel setViewFrameY:bubbleImageView.fy - _nameLabel.fh]; + + + _timeLabel.textAlignment = NSTextAlignmentLeft; _nameLabel.textAlignment = NSTextAlignmentLeft; } else { + _profilePicture.hidden = YES; [_profilePicture setViewFrameX:_profilePicture.hidden ? self.contentView.fw : self.contentView.fw - _profilePicture.fw - self.profilePicturePadding]; - [bubbleImageView setViewFrameX:_profilePicture.fx - self.bubbleWidth - self.bubbleMargin.right - xMargin]; + [bubbleImageView setViewFrameX:self.fw - self.bubbleWidth - self.bubbleMargin.right - xMargin]; //[_nameLabel setViewFrameX: bTimeLabelPadding]; - _timeLabel.textAlignment = NSTextAlignmentLeft; + _timeLabel.textAlignment = NSTextAlignmentRight; _nameLabel.textAlignment = NSTextAlignmentRight; } -// self.bubbleImageView.layer.borderColor = UIColor.redColor.CGColor; -// self.bubbleImageView.layer.borderWidth = 1; -// self.contentView.layer.borderColor = UIColor.blueColor.CGColor; -// self.contentView.layer.borderWidth = 1; -// self.cellContentView.layer.borderColor = UIColor.greenColor.CGColor; -// self.cellContentView.layer.borderWidth = 1; + + // self.bubbleImageView.layer.borderColor = UIColor.redColor.CGColor; + // self.bubbleImageView.layer.borderWidth = 1; + // self.contentView.layer.borderColor = UIColor.blueColor.CGColor; + // self.contentView.layer.borderWidth = 1; + // self.cellContentView.layer.borderColor = UIColor.greenColor.CGColor; + // self.cellContentView.layer.borderWidth = 1; } @@ -459,13 +475,24 @@ -(float) cellHeight { +(float) cellHeight: (id) message maxWidth: (float) maxWidth { UIEdgeInsets bubbleMargin = [self bubbleMargin:message]; - return [BMessageCell bubbleHeight:message maxWidth:maxWidth] + bubbleMargin.top + bubbleMargin.bottom + [self nameHeight:message]; + return [BMessageCell bubbleHeight:message maxWidth:maxWidth] + bubbleMargin.top + bubbleMargin.bottom + [self nameHeight:message] + 16; } +(float) cellHeight: (id) message { float maxWidth = [self maxTextWidth:message]; UIEdgeInsets bubbleMargin = [self bubbleMargin:message]; - return [BMessageCell bubbleHeight:message maxWidth:maxWidth] + bubbleMargin.top + bubbleMargin.bottom + [self nameHeight:message]; + id nextMessage = message.nextMessage; + + float finalHeight = [BMessageCell bubbleHeight:message maxWidth:maxWidth] + bubbleMargin.top + bubbleMargin.bottom + [self nameHeight:message] + 16; + + if (nextMessage && [nextMessage.date minutesFrom:message.date] < 10) { + if (message.date.minute == nextMessage.date.minute && [message.userModel isEqual: nextMessage.userModel]) { + //_timeLabel.text = Nil; + finalHeight -= 16; + } + } + return finalHeight; + // return [BMessageCell bubbleHeight:message maxWidth:maxWidth] + bubbleMargin.top + bubbleMargin.bottom + [self nameHeight:message]; } -(float) nameHeight { diff --git a/ChatSDKUI/Classes/Components/Message Cells/BTextMessageCell.m b/ChatSDKUI/Classes/Components/Message Cells/BTextMessageCell.m index 60e32d65..8f58a16e 100755 --- a/ChatSDKUI/Classes/Components/Message Cells/BTextMessageCell.m +++ b/ChatSDKUI/Classes/Components/Message Cells/BTextMessageCell.m @@ -70,7 +70,11 @@ -(void) setMessage: (id) message withColorWeight:(float)colorWeight #pragma Cell sizing static methods +(NSNumber *) messageContentHeight: (id) message maxWidth: (float) maxWidth { - return @([self getText: message.text heightWithFont:[UIFont systemFontOfSize:bDefaultFontSize] withWidth:[self messageContentWidth:message maxWidth:maxWidth].floatValue]); + UIFont * font = [UIFont systemFontOfSize:bDefaultFontSize]; + if(BChatSDK.config.messageTextFont) { + font = BChatSDK.config.messageTextFont; + } + return @([self getText: message.text heightWithFont:font withWidth:[self messageContentWidth:message maxWidth:maxWidth].floatValue]); } +(NSNumber *) messageContentWidth: (id) message maxWidth: (float) maxWidth { @@ -107,7 +111,11 @@ +(float) getText: (NSString *) text heightWithFont: (UIFont *) font withWidth: ( +(float) textWidth: (NSString *) text maxWidth: (float) maxWidth { if (text) { + UIFont * font = [UIFont systemFontOfSize:bDefaultFontSize]; + if(BChatSDK.config.messageTextFont) { + font = BChatSDK.config.messageTextFont; + } if (font) { return [text boundingRectWithSize:CGSizeMake(maxWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin diff --git a/ChatSDKUI/Classes/Components/Profile Pictures/BProfilePicturesViewController.m b/ChatSDKUI/Classes/Components/Profile Pictures/BProfilePicturesViewController.m index 45e556e7..978d5414 100755 --- a/ChatSDKUI/Classes/Components/Profile Pictures/BProfilePicturesViewController.m +++ b/ChatSDKUI/Classes/Components/Profile Pictures/BProfilePicturesViewController.m @@ -28,7 +28,7 @@ - (instancetype)init - (void)viewDidLoad { [super viewDidLoad]; - self.title = [NSBundle t: bProfilePictures]; + self.title = [NSBundle t: NSLocalizedString(bProfilePictures, nil)]; self.collectionView.backgroundColor = UIColor.whiteColor; if (!_user) { diff --git a/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.h b/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.h index 4a31d1e7..31fb15d9 100755 --- a/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.h +++ b/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.h @@ -41,6 +41,8 @@ UIRefreshControl * _refreshControl; + BHook * _internetConnectionHook; + // Typing Indicator NSTimer * _typingTimer; @@ -95,9 +97,9 @@ -(void) setTableViewBottomContentInset: (float) inset; -(void) registerMessageCells; - +-(void) setThreadName: (NSString *)updatedName; // To be overridden -(void) addObservers; -(void) removeObservers; - +-(void)hideRightBarButton; @end diff --git a/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.m b/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.m index 78717148..a593fe38 100755 --- a/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.m +++ b/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.m @@ -96,6 +96,18 @@ -(void) registerMessageCells { // 2 - Show's a message or the list of usersBTextInputView // 3 - Show's who's typing -(void) setupNavigationBar { + self.viewController.navigationController.navigationBar.tintColor = [UIColor blackColor]; + + +// UIBarButtonItem *backButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"< Back" style:UIBarButtonItemStylePlain target:nil action:nil]; +// [backButtonItem setTintColor:[UIColor blackColor]]; +// self.viewController.navigationItem.backBarButtonItem = backButtonItem; +// UIImage *image = [[UIImage imageNamed:@"option"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; +// self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:self action:@selector(openOptionActionSheet)]; + + UIBarButtonItem *chatOptionButton = [[UIBarButtonItem alloc] initWithTitle:@"..." style:UIBarButtonItemStylePlain target:self action:@selector(openOptionActionSheet)]; + [chatOptionButton setTintColor:[UIColor blackColor]]; + self.navigationItem.rightBarButtonItem = chatOptionButton; UIView * containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 220, 40)]; @@ -109,6 +121,9 @@ -(void) setupNavigationBar { _titleLabel.keepInsets.equal = 0; _titleLabel.keepBottomInset.equal = 15; + + + _subtitleLabel = [[UILabel alloc] init]; _subtitleLabel.textAlignment = NSTextAlignmentCenter; _subtitleLabel.font = [UIFont italicSystemFontOfSize:12.0]; @@ -124,6 +139,67 @@ -(void) setupNavigationBar { [self.navigationItem setTitleView:containerView]; } +-(void)hideRightBarButton { + self.navigationItem.rightBarButtonItem = nil; +} + +-(void)openOptionActionSheet { + UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + + // Cancel button tappped. + [self dismissViewControllerAnimated:YES completion:^{ + }]; + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"members", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + + // Distructive button tapped. + [self navigationBarTapped]; + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"invite_others", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self addUser]; + + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"edit_group_name", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self editGroupNameAlert]; + }]]; + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"leave_chat", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self leaveGroupAction]; + }]]; + + // Present action sheet. + [self presentViewController:actionSheet animated:YES completion:nil]; +} + +-(void) editGroupNameAlert { + UIAlertController * alertController = [UIAlertController alertControllerWithTitle: NSLocalizedString(@"group_name", nil) + message: nil + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.placeholder = _titleLabel.text; + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + }]; + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + + }]]; + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Ok", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + NSArray * textfields = alertController.textFields; + UITextField * groupName = textfields[0]; + if (groupName.text != nil) { + [self setTitle:groupName.text]; + [self setThreadName:groupName.text]; + } + + + }]]; + [self presentViewController:alertController animated:YES completion:nil]; +} // The options handler is responsible for displaying options to the user // when the options button is pressed. These can either be in an alert view // or a collection view shown in the keyboard overlay @@ -144,6 +220,8 @@ -(void) setupKeyboardOverlay { -(void) setTitle: (NSString *) title { _titleLabel.text = title; + [[NSUserDefaults standardUserDefaults] setObject:_titleLabel.text forKey:@"chatViewTitle"]; + [[NSUserDefaults standardUserDefaults] synchronize]; } -(void) setSubtitle: (NSString *) subtitle { @@ -336,7 +414,8 @@ -(void) typing { -(void) viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - + self.viewController.navigationController.navigationBar.tintColor = [UIColor blackColor]; + [self addObservers]; self.tabBarController.tabBar.hidden = YES; @@ -370,6 +449,12 @@ - (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterface [self reloadData]; } +-(void) viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self scrollToBottomOfTable:YES]; +} + + -(void) viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; @@ -409,9 +494,20 @@ - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger } - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:(NSIndexPath *)indexPath { +// UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"]; +// if (cell == nil) { +// cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"MyIdentifier"]; +// cell.selectionStyle = UITableViewCellSelectionStyleNone; +// +// } +// cell.textLabel.font = BChatSDK.config.messageTimeFont; +// cell.textLabel.text = @"user left the chat"; +// cell.textLabel.backgroundColor = [UIColor redColor]; +// cell.textLabel.textAlignment = NSTextAlignmentCenter; +// return cell; id message = [self messageForIndexPath:indexPath]; - + if (BChatSDK.encryption) { [BChatSDK.encryption decryptMessage:message]; } @@ -513,12 +609,12 @@ - (void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath if (!_imageViewNavigationController) { _imageViewNavigationController = [BChatSDK.ui imageViewNavigationController]; } - + // TODO: Refactor this to use the JSON keys url = cell.message.imageURL; // Only allow the user to click if the image is not still loading hence the alpha is 1 if (cell.imageView.alpha == 1 && url) { - + [cell showActivityIndicator]; cell.imageView.alpha = 0.75; @@ -543,12 +639,12 @@ - (void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath float latitude = [cell.message.meta[bMessageLatitude] floatValue]; [((id) _locationViewNavigationController.topViewController) setLatitude:latitude longitude:longitude]; - + [self.navigationController presentViewController:_locationViewNavigationController animated:YES completion:Nil]; } if(BChatSDK.videoMessage && [cell isKindOfClass:BChatSDK.videoMessage.cellClass]) { - + // Only allow the user to click if the image is not still loading hence the alpha is 1 if (cell.imageView.alpha == 1) { @@ -574,7 +670,7 @@ - (void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath } } - + if (BChatSDK.fileMessage && [cell isKindOfClass:BChatSDK.fileMessage.cellClass]) { NSDictionary * meta = cell.message.meta; @@ -683,7 +779,7 @@ -(NSArray *)tableView:(UITableView *)tableView_ editActionsForRowAtIndexPath:(NS id message = [self messageForIndexPath:indexPath]; if (message.senderIsMe) { UITableViewRowAction * button = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault - title:[NSBundle t:bDelete] + title:NSLocalizedString(@"Delete", nil) handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) { [BChatSDK.moderation deleteMessage:message.entityID]; }]; @@ -890,10 +986,45 @@ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { #pragma Utility Methods +-(void) dataUpdated { + [tableView reloadData]; + [self scrollToBottomOfTable:YES]; +} + - (void) navigationBarTapped { + [delegate navigationBarTapped]; } +- (void) openInviteScreen { + // [delegate openInviteScreen]; +} + +-(void)leaveChat { +} + +-(void) addUser { +} + +-(void)leaveGroupAction { + UIAlertController * alertController = [UIAlertController alertControllerWithTitle: NSLocalizedString(@"leave_chat_title", nil) + message: NSLocalizedString(@"leave_chat_subtitle", nil) + preferredStyle:UIAlertControllerStyleAlert]; + + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + + }]]; + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"leave", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self leaveChat]; + }]]; + [self presentViewController:alertController animated:YES completion:nil]; +} + +-(void) setThreadName: (NSString *)updatedName { +} + - (void)updateInterfaceForReachabilityStateChange { BOOL connected = BChatSDK.connectivity.isConnected; self.navigationItem.rightBarButtonItem.enabled = connected; diff --git a/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.m.orig b/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.m.orig new file mode 100644 index 00000000..b511857c --- /dev/null +++ b/ChatSDKUI/Classes/Components/SDK/ElmChatViewController.m.orig @@ -0,0 +1,1091 @@ +// +// BChatViewController.m +// Chat SDK +// +// Created by Benjamin Smiley-andrews on 21/09/2013. +// Copyright (c) 2013 deluge. All rights reserved. +// + +#import "ElmChatViewController.h" + +#import +#import + +#import +#import + + +// The distance to the bottom of the screen you need to be for the tableView to snap you to the bottom +#define bTableViewRefreshHeight 300 +#define bTableViewBottomMargin 5 + +@interface ElmChatViewController () + +@end + +@implementation ElmChatViewController + +@synthesize tableView; +@synthesize delegate; +@synthesize sendBarView = _sendBarView; +@synthesize titleLabel = _titleLabel; +@synthesize messageManager = _messageManager; + +-(instancetype) initWithDelegate: (id) delegate_ +{ + self.delegate = delegate_; + self = [super initWithNibName:@"BChatViewController" bundle:[NSBundle uiBundle]]; + if (self) { + + _messageManager = [BMessageManager new]; + + // Add a tap recognizer so when we tap the table we dismiss the keyboard + _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(viewTapped)]; + + // It should only be enabled when the keyboard is being displayed + _tapRecognizer.enabled = NO; + [self.view addGestureRecognizer:_tapRecognizer]; + + // When a user taps the title bar we want to know to show the options screen + if (BChatSDK.config.userChatInfoEnabled) { + UITapGestureRecognizer * titleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(navigationBarTapped)]; + [self.navigationItem.titleView addGestureRecognizer:titleTapRecognizer]; + } + + _notificationList = [BNotificationObserverList new]; + + _lazyReloadManager = [[BLazyReloadManager alloc] initWithTableView:tableView messageManager:_messageManager]; + _lazyReloadManager.loadMoreMessages = ^() { + if (self.delegate) { + return [self.delegate loadMoreMessages]; + } + return [RXPromise resolveWithResult:Nil]; + }; + + } + return self; +} + +// The text input view sits on top of the keyboard +-(void) setupTextInputView { + _sendBarView = [BChatSDK.ui sendBarView]; + [_sendBarView setSendBarDelegate:self]; + + [self.view addSubview:_sendBarView]; + + _sendBarView.keepBottomInset.equal = self.safeAreaBottomInset; + _sendBarView.keepLeftInset.equal = 0; + _sendBarView.keepRightInset.equal = 0; + + // Constrain the table to the top of the toolbar + tableView.keepBottomOffsetTo(_sendBarView).equal = 2; + +// tableView.layer.borderColor = [UIColor redColor].CGColor; +// tableView.layer.borderWidth = 2; + +} + +-(void) registerMessageCells { + for(NSArray * cell in BChatSDK.ui.messageCellTypes) { + [self.tableView registerClass:cell.firstObject forCellReuseIdentifier:[cell.lastObject stringValue]]; + } +} + +// The naivgation bar has three functions +// 1 - Shows the name of the chat +// 2 - Show's a message or the list of usersBTextInputView +// 3 - Show's who's typing +-(void) setupNavigationBar { + self.viewController.navigationController.navigationBar.tintColor = [UIColor blackColor]; + + +// UIBarButtonItem *backButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"< Back" style:UIBarButtonItemStylePlain target:nil action:nil]; +// [backButtonItem setTintColor:[UIColor blackColor]]; +// self.viewController.navigationItem.backBarButtonItem = backButtonItem; +// UIImage *image = [[UIImage imageNamed:@"option"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; +// self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:self action:@selector(openOptionActionSheet)]; + + UIBarButtonItem *chatOptionButton = [[UIBarButtonItem alloc] initWithTitle:@"..." style:UIBarButtonItemStylePlain target:self action:@selector(openOptionActionSheet)]; + [chatOptionButton setTintColor:[UIColor blackColor]]; + self.navigationItem.rightBarButtonItem = chatOptionButton; + + UIView * containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 220, 40)]; + + _titleLabel = [[UILabel alloc] init]; + + _titleLabel.text = [NSBundle t: bThread]; + _titleLabel.textAlignment = NSTextAlignmentCenter; + _titleLabel.font = [UIFont boldSystemFontOfSize:_titleLabel.font.pointSize]; + + [containerView addSubview:_titleLabel]; + _titleLabel.keepInsets.equal = 0; + _titleLabel.keepBottomInset.equal = 15; + + + + + _subtitleLabel = [[UILabel alloc] init]; + _subtitleLabel.textAlignment = NSTextAlignmentCenter; + _subtitleLabel.font = [UIFont italicSystemFontOfSize:12.0]; + _subtitleLabel.textColor = [UIColor lightGrayColor]; + + [containerView addSubview:_subtitleLabel]; + + _subtitleLabel.keepHeight.equal = 15; + _subtitleLabel.keepWidth.equal = 200; + _subtitleLabel.keepBottomInset.equal = 0; + _subtitleLabel.keepHorizontalCenter.equal = 0.5; + + [self.navigationItem setTitleView:containerView]; +} + +-(void)hideRightBarButton { + self.navigationItem.rightBarButtonItem = nil; +} + +-(void)openOptionActionSheet { + UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + + // Cancel button tappped. + [self dismissViewControllerAnimated:YES completion:^{ + }]; + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"members", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + + // Distructive button tapped. + [self navigationBarTapped]; + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"invite_others", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self addUser]; + + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"edit_group_name", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self editGroupNameAlert]; + }]]; + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"leave_chat", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self leaveGroupAction]; + }]]; + + // Present action sheet. + [self presentViewController:actionSheet animated:YES completion:nil]; +} + +-(void) editGroupNameAlert { + UIAlertController * alertController = [UIAlertController alertControllerWithTitle: NSLocalizedString(@"group_name", nil) + message: nil + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.placeholder = _titleLabel.text; + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + }]; + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + + }]]; + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Ok", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + NSArray * textfields = alertController.textFields; + UITextField * groupName = textfields[0]; + if (groupName.text != nil) { + [self setTitle:groupName.text]; + [self setThreadName:groupName.text]; + } + + + }]]; + [self presentViewController:alertController animated:YES completion:nil]; +} +// The options handler is responsible for displaying options to the user +// when the options button is pressed. These can either be in an alert view +// or a collection view shown in the keyboard overlay +-(void) setupKeyboardOverlay { + _keyboardOverlay = [[UIView alloc] initWithFrame:CGRectMake(0, self.view.fh, self.view.fw, 0)]; + _keyboardOverlay.backgroundColor = [UIColor whiteColor]; + + _optionsHandler = [BChatSDK.ui chatOptionsHandlerWithDelegate:self]; + + if(_optionsHandler.keyboardView) { + [_keyboardOverlay addSubview:_optionsHandler.keyboardView]; + _optionsHandler.keyboardView.keepInsets.equal = 0; + } + + _keyboardOverlay.alpha = 0; + _keyboardOverlay.userInteractionEnabled = NO; +} + +-(void) setTitle: (NSString *) title { + _titleLabel.text = title; + [[NSUserDefaults standardUserDefaults] setObject:_titleLabel.text forKey:@"chatViewTitle"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +-(void) setSubtitle: (NSString *) subtitle { + _subtitleText = subtitle; + _subtitleLabel.text = subtitle; +} + +-(void) addMessage: (id) message { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.messageManager addMessage: message]; +// NSIndexPath * indexPath = [self.messageManager addMessage: message]; +// NSIndexPath * indexPathOfPreviousMessage; + +// [self.tableView beginUpdates]; + +// if (indexPath) { +// // Also refresh the previous cell +// indexPathOfPreviousMessage = [self.messageManager indexPathForPreviousMessageInSection:message]; +// } +// +// if (indexPath) { +// // We are adding a new section +// if (indexPath.section == self.tableView.numberOfSections) { +// [self.tableView insertSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade]; +// } else { +// [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; +// } +// if (indexPathOfPreviousMessage) { +// [self.tableView reloadRowsAtIndexPaths:@[indexPathOfPreviousMessage] withRowAnimation:UITableViewRowAnimationFade]; +// } +// } +// [self.tableView endUpdates]; + [self reloadData:YES animate:YES force:message.senderIsMe]; + }); +} + +-(void) reloadDataForMessageInSection: (id) message { + [self reloadDataForIndexPath:[_messageManager indexPathForPreviousMessageInSection:message]]; +} + +-(void) reloadDataForMessage: (id) message { + [self reloadDataForIndexPath:[_messageManager indexPathForMessage:message]]; +} + +-(void) reloadDataForIndexPath: (NSIndexPath *) indexPath { + if (indexPath) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"Reload %@", NSStringFromSelector(_cmd)); + [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + }); + } +} + +-(void) addMessages: (NSArray> *) messages { + dispatch_async(dispatch_get_main_queue(), ^{ + id oldestMessage = self.messageManager.oldestMessage; + NSMutableArray * indexPaths = [NSMutableArray arrayWithArray:[self.messageManager addMessages: messages]]; + +// [self.tableView beginUpdates]; + NSLog(@"Reload %@", NSStringFromSelector(_cmd)); + [self.tableView reloadData]; + +// [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; +// +// // Also refrest the oldest message because we have loaded a message before that +// NSIndexPath * oldestMessagePath = [self.messageManager indexPathForMessage:oldestMessage]; +// if (oldestMessagePath) { +// [indexPaths addObject:oldestMessagePath]; +// } +// [self.tableView endUpdates]; +//// [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; +// +// // Move the top of the list to the old first message +// NSIndexPath * oldestMessagePath = [self.messageManager indexPathForMessage:oldestMessage]; +// if (oldestMessagePath) { +// [self.tableView scrollToRowAtIndexPath:oldestMessagePath atScrollPosition:UITableViewScrollPositionTop animated:YES]; +// } + }); +} + +-(void) removeMessage: (id) message { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.messageManager removeMessage: message]; + }); +} + +//-(void) removeMessages: (NSArray> *) messages { +// NSArray * indexPaths = [_messageManager removeMessages: messages]; +// [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; +//} + +-(void) setMessages: (NSArray *) messages { + [self setMessages:messages scrollToBottom:YES animate:YES force: NO]; +} + +-(void) setMessages: (NSArray *) messages scrollToBottom: (BOOL) scroll animate: (BOOL) animate force:(BOOL) force { + [_messageManager setMessages:messages]; + [self reloadData:scroll animate:animate force: force]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // Large titles will interfere with the custom navigation bar + if (@available(iOS 11.0, *)) { + self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; + } + + // Keep the table header at the top + if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) { + self.automaticallyAdjustsScrollViewInsets = YES; + } + + // Disable the swipe left to go back + if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) { + self.navigationController.interactivePopGestureRecognizer.enabled = NO; + } + + // Hide the tab bar when the messages are shown + self.hidesBottomBarWhenPushed = YES; + + // Add an extra 5 px padding between the top of the table and the navigation bar + // just to give the top message a bit more space + UIEdgeInsets tableInsets = tableView.contentInset; + tableInsets.top += 5; + tableInsets.bottom += bTableViewBottomMargin; + tableView.contentInset = tableInsets; + + // Add the refresh control - drag to load more messages + _refreshControl = [[UIRefreshControl alloc] init]; + [_refreshControl addTarget:self action:@selector(tableRefreshed) forControlEvents:UIControlEventValueChanged]; + [tableView addSubview:_refreshControl]; + + [self setupTextInputView]; + + [self registerMessageCells]; + + [self setupNavigationBar]; + + [self updateInterfaceForReachabilityStateChange]; + + [self setupKeyboardOverlay]; + +} + +-(void) tableRefreshed { + if ([delegate respondsToSelector:@selector(loadMoreMessages)]) { + [delegate loadMoreMessages].thenOnMain(^id(id success) { + [_refreshControl endRefreshing]; + return Nil; + }, ^id(NSError * error) { + [_refreshControl endRefreshing]; + return Nil; + }); + } +} + +-(void) startTypingWithMessage: (NSString *) message { + if(message && message.length) { + _subtitleLabel.text = message; + } + else { + [self stopTyping]; + } +} + +-(void) stopTyping { + _subtitleLabel.text = _subtitleText; +} + +-(void) setTextInputDisabled: (BOOL) disabled { + _sendBarView.hidden = disabled; +} + +-(void) setAudioEnabled:(BOOL)enabled { + [_sendBarView setAudioEnabled: enabled]; +} + +// Typing Indicator +-(void) typing { + [self setChatState:bChatStateComposing]; + // Each time the user types we reset the timer + [_typingTimer invalidate]; + _typingTimer = [NSTimer scheduledTimerWithTimeInterval:bTypingTimeout + target:self + selector:@selector(userFinishedTyping) + userInfo:nil + repeats:NO]; +} + +-(void) viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.viewController.navigationController.navigationBar.tintColor = [UIColor blackColor]; + + [self addObservers]; + + self.tabBarController.tabBar.hidden = YES; + + // The user's read the messages + [delegate markRead]; + + // This scrolls the tableview almost to the bottom + // This happens because autolayout hasn't yet been + // layed out + // The effect that this gives is that the + // view starts off almost at the bottom and + // scrolls the last bit animated (viewDidAppear) + [self scrollToBottomOfTable:NO force:YES]; + + [self setChatState:bChatStateActive]; + +// [self reloadData]; +} + +-(void) viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + // Make sure the bottom inset is correct + if (!_keyboardVisible) { + _sendBarView.keepBottomInset.equal = self.safeAreaBottomInset; + } +} + +- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{ + [self reloadData]; +} + +-(void) viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self scrollToBottomOfTable:YES]; +} + + +-(void) viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + [self removeObservers]; + + [self.delegate markRead]; + + _keyboardOverlay.alpha = 0; + _keyboardOverlay.userInteractionEnabled = NO; + + // Typing Indicator + // When the user leaves then automatically set them not to be typing in the thread + [self userFinishedTypingWithState: bChatStateInactive]; +} + +// When the view is tapped - dismiss the keyboard +-(void) viewTapped { + [self hideKeyboard]; +} + +#pragma TableView delegates + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return _messageManager.sectionCount; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return [_messageManager rowCountForSection:section]; +} + +-(id) messageForIndexPath: (NSIndexPath *) path { + return [_messageManager messageForIndexPath:path]; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { + return [_messageManager headerForSection:section]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:(NSIndexPath *)indexPath { +<<<<<<< HEAD +// UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"]; +// if (cell == nil) { +// cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"MyIdentifier"]; +// cell.selectionStyle = UITableViewCellSelectionStyleNone; +// +// } +// cell.textLabel.font = BChatSDK.config.messageTimeFont; +// cell.textLabel.text = @"user left the chat"; +// cell.textLabel.backgroundColor = [UIColor redColor]; +// cell.textLabel.textAlignment = NSTextAlignmentCenter; +// return cell; +======= + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"]; + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"MyIdentifier"]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + } + cell.textLabel.font = BChatSDK.config.messageTimeFont; + cell.textLabel.text = @"user left the chat"; + cell.textLabel.backgroundColor = [UIColor redColor]; + cell.textLabel.textAlignment = NSTextAlignmentCenter; + return cell; +>>>>>>> 40820042ec759575e23c6f339239f5f7f4b76d11 + + id message = [self messageForIndexPath:indexPath]; + + if (BChatSDK.encryption) { + [BChatSDK.encryption decryptMessage:message]; + } + + BMessageCell * messageCell; + + // We want to check if the message is a premium type but without the libraries added + // Without this check the app crashes if the user doesn't have premium cell types + if ((!BChatSDK.stickerMessage && message.type.integerValue == bMessageTypeSticker) || + (!BChatSDK.fileMessage && message.type.integerValue == bMessageTypeFile) || + (!BChatSDK.videoMessage && message.type.integerValue == bMessageTypeVideo) || + (!BChatSDK.fileMessage && message.type.integerValue == bMessageTypeFile) || + (!BChatSDK.audioMessage && message.type.integerValue == bMessageTypeAudio)) { + // This is a standard text cell + messageCell = [tableView_ dequeueReusableCellWithIdentifier:@"0"]; + } + else { + messageCell = [tableView_ dequeueReusableCellWithIdentifier:message.type.stringValue]; + } + + messageCell.navigationController = self.navigationController; + + // Add a gradient to the cells + //float colorWeight = ((float) indexPath.row / (float) self.messages.count) * 0.15 + 0.85; + float colorWeight = 1; + + [messageCell setMessage:message withColorWeight:colorWeight]; + + return messageCell; +} + +-(void) addObservers { + [self removeObservers]; + __weak __typeof__(self) weakSelf = self; + + [_notificationList add:[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:Nil queue:Nil usingBlock:^(NSNotification * notification) { + __typeof__(self) strongSelf = weakSelf; + dispatch_async(dispatch_get_main_queue(), ^{ + [strongSelf addUserToPublicThreadIfNecessary]; + }); + }]]; + + [_notificationList add: [BChatSDK.hook addHook:[BHook hook:^(NSDictionary * data) { + __typeof__(self) strongSelf = weakSelf; + [strongSelf updateInterfaceForReachabilityStateChange]; + }] withName:bHookInternetConnectivityDidChange]]; + + // Observe for keyboard appear and disappear notifications + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:Nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:Nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:Nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:Nil]; +} + +-(void) removeObservers { + [_notificationList dispose]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:Nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:Nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:Nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:Nil]; + +} + +-(void) addUserToPublicThreadIfNecessary {} + +// Layout out the bubbles. Do this after the cell's been made so we have +// access to the cell dimensions +-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { + // Allow the table to support different background colors + cell.backgroundColor = [UIColor clearColor]; + cell.contentView.backgroundColor = [UIColor clearColor]; + if ([cell respondsToSelector:@selector(willDisplayCell)]) { + [cell performSelector:@selector(willDisplayCell)]; + } +} + +// Set the message height based on the text height +- (CGFloat)tableView:(UITableView *)tableView_ heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return 40; + id message = [self messageForIndexPath:indexPath]; + if(message && [message entityID]) { + return [BMessageCell cellHeight:message]; + } + else { +// NSLog(@"Section: %i, row: %i" , indexPath.section, indexPath.row); + [_messageManager debug]; + return 0; + } +} + +- (void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + BMessageCell * cell = (BMessageCell *) [tableView_ cellForRowAtIndexPath:indexPath]; + + NSURL * url = Nil; + if ([cell isKindOfClass:[BImageMessageCell class]]) { + + url = cell.message.imageURL; + + if (!_imageViewNavigationController) { + _imageViewNavigationController = [BChatSDK.ui imageViewNavigationController]; + } + + // TODO: Refactor this to use the JSON keys + url = cell.message.imageURL; + // Only allow the user to click if the image is not still loading hence the alpha is 1 + if (cell.imageView.alpha == 1 && url) { + + [cell showActivityIndicator]; + cell.imageView.alpha = 0.75; + + __weak __typeof__(self) weakSelf = self; + [cell.imageView sd_setImageWithURL:url placeholderImage:cell.imageView.image completed: ^(UIImage * image, NSError * error, SDImageCacheType cacheType, NSURL * imageURL) { + __typeof__(self) strongSelf = weakSelf; + + [cell hideActivityIndicator]; + cell.imageView.alpha = 1; + + [((id) strongSelf->_imageViewNavigationController.topViewController) setImage: image]; + [strongSelf.navigationController presentViewController:strongSelf->_imageViewNavigationController animated:YES completion:Nil]; + }]; + } + } + if ([cell isKindOfClass:[BLocationCell class]]) { + if (!_locationViewNavigationController) { + _locationViewNavigationController = [BChatSDK.ui locationViewNavigationController]; + } + + float longitude = [cell.message.meta[bMessageLongitude] floatValue]; + float latitude = [cell.message.meta[bMessageLatitude] floatValue]; + + [((id) _locationViewNavigationController.topViewController) setLatitude:latitude longitude:longitude]; + + [self.navigationController presentViewController:_locationViewNavigationController animated:YES completion:Nil]; + } + + if(BChatSDK.videoMessage && [cell isKindOfClass:BChatSDK.videoMessage.cellClass]) { + + // Only allow the user to click if the image is not still loading hence the alpha is 1 + if (cell.imageView.alpha == 1) { + + NSURL * url = [NSURL URLWithString:cell.message.meta[bMessageVideoURL]]; + + // Add an activity indicator while the image is loading + UIActivityIndicatorView * activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + activityIndicator.frame = CGRectMake(cell.imageView.fw/2 - 20, cell.imageView.fh/2 -20, 40, 40); + [activityIndicator startAnimating]; + + [cell.imageView addSubview:activityIndicator]; + [cell.imageView bringSubviewToFront:activityIndicator]; + + // Make sure the audio plays even if we're in silent mode + [[AVAudioSession sharedInstance] + setCategory: AVAudioSessionCategoryPlayback + error: nil]; + + AVPlayer * player = [[AVPlayer alloc] initWithURL:url]; + AVPlayerViewController * playerController = [[AVPlayerViewController alloc] init]; + playerController.player = player; + [self presentViewController:playerController animated:YES completion:Nil]; + + } + } + + if (BChatSDK.fileMessage && [cell isKindOfClass:BChatSDK.fileMessage.cellClass]) { + NSDictionary * meta = cell.message.meta; + + if (![BFileCache isFileCached:cell.message.entityID]) { + [cell.imageView setImage:[NSBundle imageNamed:@"file.png" bundle:BChatSDK.fileMessage.bundle]]; + } + + [cell showActivityIndicator]; + + NSURL * url = [NSURL URLWithString:meta[bMessageFileURL]]; + [BFileCache cacheFileFromURL:url withFileName:meta[bMessageText] andCacheName:cell.message.entityID] + .thenOnMain(^id(NSURL * cacheUrl) { + NSLog(@"Cache URL: %@", [cacheUrl absoluteString]); + [cell setMessage:cell.message]; + + [cell hideActivityIndicator]; + + [self presentDocumentInteractionViewControllerWithURL:cacheUrl andName:nil]; + return nil; + }, ^id(NSError *error) { + NSLog(@"Error: %@", error.localizedDescription); + [cell hideActivityIndicator]; + return nil; + }); + } +} + +- (void)presentDocumentInteractionViewControllerWithURL:(NSURL *)url andName:(NSString *)name { + _documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:url]; + [_documentInteractionController setDelegate:self]; + if (name) { + [_documentInteractionController setName:name]; + } + [_documentInteractionController presentPreviewAnimated:YES]; +} + +-(BOOL) showOptions { + + // Needed for keyboard overlay to raise keyboard + [_sendBarView becomeTextViewFirstResponder]; + + if (_optionsHandler.keyboardView) { + _keyboardOverlay.alpha = 1; + _keyboardOverlay.userInteractionEnabled = YES; + } + + return [_optionsHandler show]; +} + +-(BOOL) hideOptions { + [_sendBarView becomeTextViewFirstResponder]; + + _keyboardOverlay.alpha = 0; + _keyboardOverlay.userInteractionEnabled = NO; + return [_optionsHandler hide]; +} + +-(void) didResizeTextInputViewWithDelta:(float)delta { +// if (fabsf(delta) > 0.01) { +// [self setTableViewBottomContentInsetWithDelta:delta]; +// [self scrollToBottomOfTable:NO]; +// } +} + +-(void) hideKeyboard { + [_sendBarView resignTextViewFirstResponder]; +} + +#pragma BChatOptionDelegate + +-(UIViewController *) currentViewController { + return self; +} + +#pragma Picture selection + +-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath { + + id message = [self messageForIndexPath:indexPath]; + + return message.flagged.intValue ? bUnflag : [NSBundle t:bFlag]; +} + +// Check that this is called for iOS7 +- (void)tableView:(UITableView *)tableView_ commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { + + id message = [self messageForIndexPath:indexPath]; + + [delegate setMessageFlagged:message isFlagged:message.flagged.intValue].thenOnMain(^id(id success) { + // Reload the tableView and not [self reloadData] so we don't go to the bottom of the tableView + [self reloadDataForMessage:message]; + return Nil; + }, Nil); + + [tableView_ setEditing:NO animated:YES]; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + return YES; +} + +// This only works for iOS8 +-(NSArray *)tableView:(UITableView *)tableView_ editActionsForRowAtIndexPath:(NSIndexPath *)indexPath { + __weak __typeof__(self) weakSelf = self; + + id message = [self messageForIndexPath:indexPath]; + if (message.senderIsMe) { + UITableViewRowAction * button = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault + title:[NSBundle t:bDelete] + handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) { + [BChatSDK.moderation deleteMessage:message.entityID]; + }]; + + button.backgroundColor = [UIColor redColor]; + return @[button]; + + } + else { + NSString * flagTitle = message.flagged.intValue ? [NSBundle t:bUnflag] : [NSBundle t:bFlag]; + + UITableViewRowAction * button = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:flagTitle handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) { + __typeof__(self) strongSelf = weakSelf; + [strongSelf.delegate setMessageFlagged:message isFlagged:message.flagged.intValue].thenOnMain(^id(id success) { + // Reload the tableView and not [self reloadData] so we don't go to the bottom of the tableView + [strongSelf reloadDataForMessage:message]; + return Nil; + }, Nil); + + }]; + + button.backgroundColor = message.flagged.intValue ? [UIColor darkGrayColor] : [UIColor redColor]; + + return @[button]; + } + +} + +#pragma Handle keyboard + +// Move the toolbar up +-(void) keyboardWillShow: (NSNotification *) notification { + + _keyboardVisible = YES; + + // Get the keyboard size + CGRect keyboardBounds = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGRect keyboardBoundsConverted = [self.view convertRect:keyboardBounds toView:Nil]; + + // Get the duration and curve from the notification + NSNumber *duration = [notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; + NSNumber *curve = [notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey]; + + // Set the initial position + [UIView beginAnimations:Nil context:Nil]; + [UIView setAnimationDuration:0]; + _keyboardOverlay.frame = CGRectMake(0, self.view.fh, self.view.fw, 0); + [UIView commitAnimations]; + + // Set the new constraints + _sendBarView.keepBottomInset.equal = keyboardBoundsConverted.size.height; + + [[UIApplication sharedApplication].windows.lastObject addSubview: _keyboardOverlay]; + _keyboardOverlay.frame = keyboardBounds; + + // Animate using this style because for some reason + // using blocks doesn't give a smooth animation + [UIView beginAnimations:Nil context:Nil]; + + [UIView setAnimationBeginsFromCurrentState:YES]; + [UIView setAnimationDuration:duration.doubleValue]; + [UIView setAnimationCurve:curve.integerValue]; + + float contentOffsetY = tableView.contentOffset.y + keyboardBoundsConverted.size.height - self.safeAreaBottomInset; + + [tableView setContentOffset:CGPointMake(0, contentOffsetY)]; + + [UIView setAnimationsEnabled:NO]; + for(UITableViewCell * cell in tableView.visibleCells) { + [cell layoutIfNeeded]; + } + [UIView setAnimationsEnabled:YES]; + + + [self.view layoutIfNeeded]; + + [UIView commitAnimations]; + +} + +-(void) keyboardWillHide: (NSNotification *) notification { + + NSNumber *duration = [notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; + NSNumber *curve = [notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey]; + + CGRect keyboardBounds = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGRect keyboardBoundsConverted = [self.view convertRect:keyboardBounds toView:Nil]; + + _keyboardOverlay.frame = keyboardBounds; + + _sendBarView.keepBottomInset.equal = self.safeAreaBottomInset; + [_sendBarView setNeedsUpdateConstraints]; + + [UIView beginAnimations:Nil context:Nil]; + + [UIView setAnimationBeginsFromCurrentState:YES]; + [UIView setAnimationDuration:duration.doubleValue]; + [UIView setAnimationCurve:curve.integerValue]; + + + [UIView setAnimationsEnabled:NO]; + + for(UITableViewCell * cell in tableView.visibleCells) { + [cell setNeedsLayout]; + [cell layoutIfNeeded]; + } + [UIView setAnimationsEnabled:YES]; + + float contentOffsetY = tableView.contentOffset.y - keyboardBoundsConverted.size.height + self.safeAreaBottomInset; + [tableView setContentOffset:CGPointMake(0, contentOffsetY)]; + + [self.view layoutIfNeeded]; + + [UIView commitAnimations]; + + // Disable the gesture recognizer so cell taps are recognized + _tapRecognizer.enabled = NO; + +} + +-(void) keyboardDidShow: (NSNotification *) notification { + + // Get the keyboard size + CGRect keyboardBounds = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGRect keyboardBoundsConverted = [self.view convertRect:keyboardBounds toView:Nil]; + + // Enable the tap gesture recognizer to hide the keyboard + _tapRecognizer.enabled = YES; +} + + +-(float) safeAreaBottomInset { + // Move the text input up to avoid the bottom area + if (@available(iOS 11, *)) { + return self.view.safeAreaInsets.bottom; + } + return 0; +} + +-(void) keyboardDidHide: (NSNotification *) notification { + [_keyboardOverlay removeFromSuperview]; + _keyboardVisible = NO; +} + +-(void) scrollToBottomOfTable: (BOOL) animated { + [self scrollToBottomOfTable:animated force:NO]; +} + +-(void) scrollToBottomOfTable: (BOOL) animated force: (BOOL) force { + dispatch_async(dispatch_get_main_queue(), ^{ + NSInteger lastSection = self.tableView.numberOfSections - 1; + if (lastSection < 0) { + return; + } + NSInteger lastRow = [self.tableView numberOfRowsInSection:lastSection] - 1; + if (lastRow < 0) { + return; + } + + BOOL scroll = NO; + if ((self.tableView.contentSize.height - self.tableView.frame.size.height) - self.tableView.contentOffset.y <= bTableViewRefreshHeight) { + scroll = YES; + } + + if (scroll || force) { + NSIndexPath * indexPath = [NSIndexPath indexPathForItem:lastRow inSection:lastSection]; + [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:animated]; + } + }); +} + +-(void) reloadData { + [self reloadData:YES animate: YES force:NO]; +} + +-(void) reloadData: (BOOL) scroll animate: (BOOL) animate force: (BOOL) force { + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView transitionWithView: self.tableView + duration: animate ? 0.2f : 0 + options: UIViewAnimationOptionTransitionCrossDissolve + animations: ^(void) { + NSLog(@"Reload %@", NSStringFromSelector(_cmd)); + [self.tableView reloadData]; + } + completion: ^(BOOL finished) { + if (scroll) { + [self scrollToBottomOfTable:animate force:force]; + } + } ]; + }); +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + [_lazyReloadManager scrollViewDidScroll: scrollView]; +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + [_lazyReloadManager scrollViewWillBeginDragging:scrollView]; +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + [_lazyReloadManager scrollViewDidEndDecelerating:scrollView]; +} + +#pragma Utility Methods + +-(void) dataUpdated { + [tableView reloadData]; + [self scrollToBottomOfTable:YES]; +} + +- (void) navigationBarTapped { + + [delegate navigationBarTapped]; +} + +- (void) openInviteScreen { + // [delegate openInviteScreen]; +} + +-(void)leaveChat { +} + +-(void) addUser { +} + +-(void)leaveGroupAction { + UIAlertController * alertController = [UIAlertController alertControllerWithTitle: NSLocalizedString(@"leave_chat_title", nil) + message: NSLocalizedString(@"leave_chat_subtitle", nil) + preferredStyle:UIAlertControllerStyleAlert]; + + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + + }]]; + + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"leave", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self leaveChat]; + }]]; + [self presentViewController:alertController animated:YES completion:nil]; +} + +-(void) setThreadName: (NSString *)updatedName { +} + +- (void)updateInterfaceForReachabilityStateChange { + BOOL connected = BChatSDK.connectivity.isConnected; + self.navigationItem.rightBarButtonItem.enabled = connected; +} + +-(void) userFinishedTyping { + [self userFinishedTypingWithState:bChatStateActive]; +} + +-(void) setChatState: (bChatState) state { + if (state != _chatState) { + [delegate setChatState:state]; + } + _chatState = state; +} + +-(void) userFinishedTypingWithState: (bChatState) state { + [self setChatState:state]; + [_typingTimer invalidate]; +} + +-(UIViewController *) viewController { + return self; +} + +-(void) dealloc { + self.delegate = Nil; + [_sendBarView setSendBarDelegate:Nil]; + self.tableView.delegate = Nil; + self.tableView.dataSource = Nil; + [_typingTimer invalidate]; + _typingTimer = Nil; +} + +#pragma UIDocumentInteractionControllerDelegate + +- (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller { + return self; +} + +- (UIView *)documentInteractionControllerViewForPreview:(UIDocumentInteractionController *)controller { + return self.view; +} + +- (CGRect)documentInteractionControllerRectForPreview:(UIDocumentInteractionController *)controller { + return self.view.frame; +} + +@end diff --git a/ChatSDKUI/Classes/Components/Threads/BPrivateThreadsViewController.m b/ChatSDKUI/Classes/Components/Threads/BPrivateThreadsViewController.m index 97a3b6d4..c2ac76db 100755 --- a/ChatSDKUI/Classes/Components/Threads/BPrivateThreadsViewController.m +++ b/ChatSDKUI/Classes/Components/Threads/BPrivateThreadsViewController.m @@ -11,6 +11,7 @@ #import #import + @interface BPrivateThreadsViewController () @end @@ -21,8 +22,11 @@ -(instancetype) init { self = [super initWithNibName:Nil bundle:[NSBundle uiBundle]]; if (self) { - self.title = [NSBundle t:bConversations]; - self.tabBarItem.image = [NSBundle uiImageNamed: @"icn_30_chat.png"]; + //Changed + self.title = [NSBundle t: NSLocalizedString(@"Chat", nil)]; + self.tabBarItem.image = [NSBundle uiImageNamed:@"chat_icon_unSelected@2x.png"]; + self.tabBarItem.selectedImage = [NSBundle uiImageNamed:@"chat_icon@2x.png"]; + self.tabBarItem.titlePositionAdjustment = UIOffsetMake(0, -4.0); } return self; @@ -30,16 +34,20 @@ -(instancetype) init - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:YES]; - - _editButton = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t:bEdit] + _editButton = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t: NSLocalizedString(bEdit, nil)] style:UIBarButtonItemStylePlain target:self action:@selector(editButtonPressed:)]; // If we have no threads we don't have the edit button self.navigationItem.leftBarButtonItem = _threads.count ? _editButton : nil; + self.navigationController.navigationBar.tintColor = [UIColor blackColor]; + } + + + - (void)viewDidLoad { [super viewDidLoad]; @@ -47,6 +55,7 @@ - (void)viewDidLoad { self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(createThread)]; + [self setExtendedLayoutIncludesOpaqueBars:YES]; } -(void) createThread { @@ -54,9 +63,12 @@ -(void) createThread { } -(void) createPrivateThread { +// [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"isPoped"]; +// [[NSUserDefaults standardUserDefaults] synchronize]; + __weak __typeof__(self) weakSelf = self; - + UINavigationController * nav = [BChatSDK.ui friendsNavigationControllerWithUsersToExclude:@[] onComplete:^(NSArray * users, NSString * groupName){ __typeof__(self) strongSelf = weakSelf; @@ -76,7 +88,8 @@ -(void) createPrivateThread { }]; - [self presentViewController:nav animated:YES completion:Nil]; + + [[self navigationController] pushViewController:nav.viewControllers[0] animated:true]; } -(void) editButtonPressed: (UIBarButtonItem *) item { @@ -96,6 +109,7 @@ -(void) reloadData { [_threads removeAllObjects]; [_threads addObjectsFromArray:[BChatSDK.core threadsWithType:bThreadFilterPrivateThread includeDeleted:NO]]; [super reloadData]; + } @end diff --git a/ChatSDKUI/Classes/Components/Threads/BThreadCell.m b/ChatSDKUI/Classes/Components/Threads/BThreadCell.m index 0c55d29b..fbc48b89 100755 --- a/ChatSDKUI/Classes/Components/Threads/BThreadCell.m +++ b/ChatSDKUI/Classes/Components/Threads/BThreadCell.m @@ -28,7 +28,7 @@ - (void)awakeFromNib self.unreadMessagesLabel.layer.cornerRadius = 5; self.unreadMessagesLabel.clipsToBounds = YES; - + [self.unreadMessagesLabel setHidden:YES]; self.preservesSuperviewLayoutMargins = NO; self.separatorInset = UIEdgeInsetsZero; self.layoutMargins = UIEdgeInsetsZero; diff --git a/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.h b/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.h index 7fca4a17..f4bbeea6 100755 --- a/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.h +++ b/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.h @@ -11,7 +11,6 @@ @class BNotificationObserverList; @class BHook; - @interface BThreadsViewController : UIViewController { UIBarButtonItem * _editButton; diff --git a/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.m b/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.m index d042fae2..b68f8044 100755 --- a/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.m +++ b/ChatSDKUI/Classes/Components/Threads/BThreadsViewController.m @@ -12,6 +12,7 @@ #import #import +#import "EmptyChatView.h" #define bCellIdentifier @"bCellIdentifier" @@ -49,12 +50,15 @@ - (void)viewDidLoad { tableView.delegate = self; tableView.dataSource = self; - [self.view addSubview:tableView]; - - tableView.keepInsets.equal = 0; + EmptyChatView *emptyView = [[EmptyChatView alloc] initWithNibName:@"EmptyChatView" bundle:[NSBundle uiBundle]]; + [self.view addSubview:emptyView.view]; + emptyView.view.keepInsets.equal = 0; + [emptyView setText:NSLocalizedString(@"main_chat_empty_view_title_text", nil) setSubTitle:NSLocalizedString(@"main_chat_empty_view_subtitle_text", nil) setEmptyImage:[NSBundle uiImageNamed: @"empty_chat_view@2x.png"]]; + [self.view addSubview:tableView]; + tableView.keepInsets.equal = 0; // Sets the back button for the thread views as back meaning we have more space for the title - self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t:bBack] style:UIBarButtonItemStylePlain target:nil action:nil]; + self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t: NSLocalizedString(bBack, nil)] style:UIBarButtonItemStylePlain target:nil action:nil]; [tableView registerNib:[UINib nibWithNibName:@"BThreadCell" bundle:[NSBundle uiBundle]] forCellReuseIdentifier:bCellIdentifier]; @@ -121,6 +125,10 @@ -(void) removeObservers { [_notificationList dispose]; } +-(void) showEmptyView:(BOOL)showView { + [self.tableView setHidden:showView]; +} + -(void) createThread { NSLog(@"This must be overridden"); assert(NO); @@ -154,10 +162,10 @@ -(void) toggleEditing { -(void) setEditingEnabled: (BOOL) enabled { if (enabled) { - [_editButton setTitle:[NSBundle t:bDone]]; + [_editButton setTitle:[NSBundle t: NSLocalizedString(bDone, nil)]]; } else { - [_editButton setTitle:[NSBundle t:bEdit]]; + [_editButton setTitle:[NSBundle t: NSLocalizedString(bEdit, nil)]]; } [tableView setEditing:enabled animated:YES]; } @@ -174,7 +182,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:( NSDate * threadDate = thread.orderDate; - NSString * text = @"";// [NSBundle t:bNoMessages]; + NSString * text = [NSBundle t: NSLocalizedString(bNoMessages, nil)]; id newestMessage = thread.newestMessage; if (newestMessage) { @@ -188,16 +196,32 @@ - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:( cell.dateLabel.text = @""; } - if(BChatSDK.config.threadTimeFont) { - cell.dateLabel.font = BChatSDK.config.threadTimeFont; - } - if(BChatSDK.config.threadTitleFont) { - cell.titleLabel.font = BChatSDK.config.threadTitleFont; + if (thread.unreadMessageCount) { + if(BChatSDK.config.threadTimeFont) { + cell.dateLabel.font = BChatSDK.config.threadTimeFont; + } + + if(BChatSDK.config.threadTitleFont) { + cell.titleLabel.font = BChatSDK.config.threadTitleFont; + } + + if(BChatSDK.config.threadSubtitleFont) { + cell.messageTextView.font = BChatSDK.config.threadSubtitleFont; + } } - - if(BChatSDK.config.threadSubtitleFont) { - cell.messageTextView.font = BChatSDK.config.threadSubtitleFont; + else { + if(BChatSDK.config.unreadThreadTimeFont) { + cell.dateLabel.font = BChatSDK.config.unreadThreadTimeFont; + } + + if(BChatSDK.config.unreadThreadTitleFont) { + cell.titleLabel.font = BChatSDK.config.unreadThreadTitleFont; + } + + if(BChatSDK.config.unreadThreadSubtitleFont) { + cell.messageTextView.font = BChatSDK.config.unreadThreadSubtitleFont; + } } cell.titleLabel.text = thread.displayName ? thread.displayName : [NSBundle t: bDefaultThreadName]; @@ -214,11 +238,20 @@ - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:( }, Nil); } - // cell.unreadView.hidden = !thread.unreadMessageCount; + cell.unreadView.hidden = true; - int unreadCount = thread.unreadMessageCount; - cell.unreadMessagesLabel.hidden = !unreadCount; - cell.unreadMessagesLabel.text = [@(unreadCount) stringValue]; +// int unreadCount = thread.unreadMessageCount; +// cell.unreadMessagesLabel.hidden = !unreadCount; +// cell.unreadMessagesLabel.text = [@(unreadCount) stringValue]; + + //Adding online status + id userIn = newestMessage.userModel; + if([userIn.online isEqualToNumber:[NSNumber numberWithBool:YES]]){ + [cell setIsOnline:true]; + } + else{ + [cell setIsOnline:false]; + } // Add the typing indicator NSString * typingText = _threadTypingMessages[thread.entityID]; @@ -299,6 +332,12 @@ -(void) reloadData { [_threads sortUsingComparator:^(idt1, id t2) { return [t2.orderDate compare:t1.orderDate]; }]; + if ([_threads count] > 0) { + [self showEmptyView:false]; + } + else { + [self showEmptyView:true]; + } [tableView reloadData]; } diff --git a/ChatSDKUI/Classes/Components/View Controllers/BAppTabBarController.m b/ChatSDKUI/Classes/Components/View Controllers/BAppTabBarController.m index 863f1449..389c6282 100755 --- a/ChatSDKUI/Classes/Components/View Controllers/BAppTabBarController.m +++ b/ChatSDKUI/Classes/Components/View Controllers/BAppTabBarController.m @@ -176,6 +176,7 @@ -(int) unreadMessagesCount: (bThreadType) type { for (id message in thread.allMessages) { if (!message.isRead) { i++; + break; } } } @@ -188,12 +189,12 @@ -(void) setBadge: (int) badge forViewController: (UIViewController *) controller if (index != NSNotFound) { // Using self.tabbar will correctly set the badge for the specific index NSString * badgeString = badge == 0 ? Nil : [NSString stringWithFormat:@"%i", badge]; - [self.tabBar.items objectAtIndex:index].badgeValue = badgeString; + [self.tabBar.items objectAtIndex:index].badgeValue = badgeString; } } -(void) setPrivateThreadsBadge: (NSInteger) badge { - [self setBadge:badge forViewController:BChatSDK.ui.privateThreadsViewController]; + [self setBadge:badge forViewController:BChatSDK.ui.privateThreadsViewController]; // Save the value to defaults [[NSUserDefaults standardUserDefaults] setInteger:badge forKey:bMessagesBadgeValueKey]; diff --git a/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.h b/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.h index 1dce4465..9774eb69 100755 --- a/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.h +++ b/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.h @@ -36,6 +36,7 @@ @property (weak, nonatomic) IBOutlet UITableView *tableView; @property (nonatomic, readwrite, copy) void (^usersToInvite)(NSArray * users, NSString * groupName); @property (nonatomic, readwrite) NSString * rightBarButtonActionTitle; + @property (nonatomic, readwrite) NSArray * (^overrideContacts)(void); @property (weak, nonatomic) IBOutlet VENTokenField * _tokenField; diff --git a/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.m b/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.m index 1f7e1048..5b4a19d3 100755 --- a/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.m +++ b/ChatSDKUI/Classes/Components/View Controllers/BFriendsListViewController.m @@ -10,6 +10,7 @@ #import #import +#import "EmptyChatView.h" #define bUserCellIdentifier @"bUserCellIdentifier" @@ -36,17 +37,52 @@ @implementation BFriendsListViewController -(instancetype) initWithUsersToExclude: (NSArray *) users onComplete: (void(^)(NSArray * users, NSString * name)) action { if ((self = [self init])) { - self.title = [NSBundle t:bPickFriends]; - [_contactsToExclude addObjectsFromArray:users]; + + // BOOL isPoped = [[NSUserDefaults standardUserDefaults] + // boolForKey:@"isPoped"]; + if (users.count == 0) + { + self.title = [NSBundle t: NSLocalizedString(bPickFriends, nil)];//[NSBundle t:bPickFriends]; + } + else + { + self.title = [NSBundle t: NSLocalizedString(bInviteFriend, nil)]; + } + [_contactsToExclude addObjectsFromArray:users]; self.usersToInvite = action; } return self; } + + +- (BOOL)isModal { + if([[self presentingViewController] presentedViewController] == self) + return YES; + if([[[self navigationController] presentingViewController] presentedViewController] == [self navigationController]) + return YES; + if([[[self tabBarController] presentingViewController] isKindOfClass:[UITabBarController class]]) + return YES; + + return NO; +} + + -(instancetype) init { self = [super initWithNibName:@"BFriendsListViewController" bundle:[NSBundle uiBundle]]; if (self) { - self.title = [NSBundle t:bPickFriends]; + + // BOOL isPoped = [[NSUserDefaults standardUserDefaults] + // boolForKey:@"isPoped"]; + if ([self isModal]) + { + self.title = [NSBundle t: NSLocalizedString(bPickFriends, nil)];//[NSBundle t:bPickFriends]; + } + else + { + self.title = [NSBundle t: NSLocalizedString(bInviteFriend, nil)]; + } + _selectedContacts = [NSMutableArray new]; _contacts = [NSMutableArray new]; _contactsToExclude = [NSMutableArray new]; @@ -59,18 +95,35 @@ - (void)viewDidLoad { groupNameTextField.placeholder = [NSBundle t:bGroupName]; groupNameTextField.delegate = self; - - self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t:bBack] style:UIBarButtonItemStylePlain target:self action:@selector(dismissView)]; + + if ([self isMovingFromParentViewController]){ + + } + // BOOL isPoped = [[NSUserDefaults standardUserDefaults] + // boolForKey:@"isPoped"]; + if ([self isModal]) + { + UIImage *image = [[UIImage imageNamed:@"cross"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:self action:@selector(dismissView)]; + } + + //self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t:bImageSaved] style:UIBarButtonItemStylePlain target:self action:@selector(dismissView)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:self.getRightBarButtonActionTitle style:UIBarButtonItemStylePlain target:self action:@selector(composeMessage)]; + self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle: [NSBundle t: NSLocalizedString(bBack, nil)] + style:UIBarButtonItemStylePlain + target:nil + action:@selector(backButtonPressed)]; // Takes into account the status and navigation bar self.edgesForExtendedLayout = UIRectEdgeNone; self.names = [NSMutableArray array]; _tokenField.delegate = self; + // _tokenField.inputTextFieldAccessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"search"]]; + _tokenField.dataSource = self; - _tokenField.placeholderText = [NSBundle t:bEnterNamesHere]; - _tokenField.toLabelText = [NSBundle t:bTo]; + _tokenField.placeholderText =[NSBundle t: NSLocalizedString(bEnterNamesHere, nil)]; // [NSBundle t:bEnterNamesHere]; + _tokenField.toLabelText = [NSBundle t: NSLocalizedString(bTo, nil)]; _tokenField.userInteractionEnabled = YES; [_tokenField setColorScheme:[UIColor colorWithRed:61/255.0f green:149/255.0f blue:206/255.0f alpha:1.0f]]; @@ -78,13 +131,21 @@ - (void)viewDidLoad { _tokenView.layer.borderWidth = 0.5; _tokenView.layer.borderColor = [UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1.0].CGColor; + //Add empty view + EmptyChatView *emptyView = [[EmptyChatView alloc] initWithNibName:@"EmptyChatView" bundle:[NSBundle uiBundle]]; + [self.view insertSubview:emptyView.view atIndex:0]; + // [self.view insertSubview:emptyView.view belowSubview:self.tableView]; + //[self.view addSubview:emptyView.view]; + emptyView.view.keepInsets.equal = 0; + [emptyView setText:NSLocalizedString(@"contacts_empty_view_title_text", nil) setSubTitle:NSLocalizedString(@"contacts_empty_view_subtitle_text", nil) setEmptyImage:[NSBundle uiImageNamed: @"empty_chat_view@2x.png"]]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:Nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:Nil]; [self reloadData]; [tableView registerNib:[UINib nibWithNibName:@"BUserCell" bundle:[NSBundle uiBundle]] forCellReuseIdentifier:bUserCellIdentifier]; - + [self setGroupNameHidden:YES duration:0]; } @@ -92,12 +153,13 @@ -(NSString *) getRightBarButtonActionTitle { if (self.rightBarButtonActionTitle) { return self.rightBarButtonActionTitle; } - else if (_selectedContacts.count <= 1) { - return [NSBundle t: bCompose]; - } - else { - return [NSBundle t: bCreateGroup]; - } + return [NSBundle t: NSLocalizedString(bCompose, nil)]; +// else if (_selectedContacts.count <= 1) { +// return [NSBundle t: bCompose]; +// } +// else { +// return [NSBundle t: bCreateGroup]; +// } } -(void) updateRightBarButtonActionTitle { @@ -106,7 +168,8 @@ -(void) updateRightBarButtonActionTitle { - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:YES]; - + self.navigationController.navigationBar.tintColor = [UIColor blackColor]; + __weak __typeof__(self) weakSelf = self; _internetConnectionHook = [BHook hook:^(NSDictionary * data) { __typeof__(self) strongSelf = weakSelf; @@ -115,7 +178,7 @@ - (void)viewWillAppear:(BOOL)animated { } }]; [BChatSDK.hook addHook:_internetConnectionHook withName:bHookInternetConnectivityDidChange]; - + [self reloadData]; } @@ -123,6 +186,10 @@ -(void) viewDidLayoutSubviews { [super viewDidLayoutSubviews]; } +-(void) showEmptyView:(BOOL)showView { + [self.tableView setHidden:showView]; +} + - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { NSString * newString = [textField.text stringByReplacingCharactersInRange:range withString:string]; self.navigationItem.rightBarButtonItem.enabled = newString.length; @@ -152,16 +219,59 @@ -(void) composeMessage { return; } else { - [self dismissViewControllerAnimated:YES completion:^{ + //Create Group + if (_selectedContacts.count > 1) + { + UIAlertController * alertController = [UIAlertController alertControllerWithTitle: @"Group Name" + message: nil + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.placeholder = @"Group Name"; + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + }]; + + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + + }]]; + + [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + NSArray * textfields = alertController.textFields; + UITextField * groupName = textfields[0]; + self.usersToInvite(_selectedContacts, groupName.text); + [self dismissViewControllerAnimated:YES completion:^{ +// if (self.usersToInvite != Nil) { + +// } + }]; + + }]]; + [self presentViewController:alertController animated:YES completion:nil]; + } + else //1-1 chat + { + [self.navigationController popViewControllerAnimated:true]; if (self.usersToInvite != Nil) { self.usersToInvite(_selectedContacts, groupNameTextField.text); } - }]; +// [self dismissViewControllerAnimated:YES completion:^{ +// if (self.usersToInvite != Nil) { +// self.usersToInvite(_selectedContacts, groupNameTextField.text); +// } +// }]; + } + + } } #pragma UITableView delegate +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + [_tokenField resignFirstResponder]; +} + + - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return bSectionCount; } @@ -175,15 +285,16 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - if (section == bContactsSection) { - return _contacts.count ? [NSBundle t:bContacts] : @""; - } +// if (section == bContactsSection) { +// return _contacts.count ? [NSBundle t:bContacts] : @""; +// } return @""; } - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:(NSIndexPath *)indexPath { + BUserCell * cell = [tableView_ dequeueReusableCellWithIdentifier:bUserCellIdentifier]; @@ -191,29 +302,66 @@ - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:( if (indexPath.section == bContactsSection) { user = _contacts[indexPath.row]; } - + if ([_selectedContacts containsObject:user] || [_contactsToExclude containsObject:user]){ + [cell setSelectedImage]; + } + else{ + [cell setDeSelectedImage]; + } [cell setUser:user]; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - return 50; + return 71; } - (void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + id user; +// if ([_contactsToExclude containsObject:user]){ +// return; +// } if (indexPath.section == bContactsSection) { - [self selectUser:_contacts[indexPath.row]]; + user = _contacts[indexPath.row]; } + BOOL value = [[user.meta metaValueForKey:@"can_message"] boolValue]; + if (value == false){ + [self showAlertMessage]; + return; + } + if (indexPath.section == bContactsSection) { + [self selectUser:user]; + } + self.navigationItem.rightBarButtonItem.enabled = _selectedContacts.count; [tableView_ deselectRowAtIndexPath:indexPath animated:YES]; + [tableView_ reloadData]; - [UIView animateWithDuration:0.2 animations:^{ - _tokenView.keepHeight.equal = _tokenField.bounds.size.height; - }]; + // [UIView animateWithDuration:0.2 animations:^{ + // _tokenView.keepHeight.equal = _tokenField.bounds.size.height; + // }]; - [self reloadData]; + // [self reloadData]; +} + +-(void) showAlertMessage { + UIAlertController * alert = [UIAlertController + alertControllerWithTitle:NSLocalizedString(@"unavailable", nil) + message:NSLocalizedString(@"application_not_installed", nil) + preferredStyle:UIAlertControllerStyleAlert]; + + + UIAlertAction* okButton = [UIAlertAction + actionWithTitle:NSLocalizedString(@"Ok", nil) + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + //Handle no, thanks button + }]; + + [alert addAction:okButton]; + [self presentViewController:alert animated:YES completion:nil]; } #pragma mark - VENTokenFieldDelegate @@ -233,7 +381,7 @@ - (void)tokenField:(VENTokenField *)tokenField didChangeText:(NSString *)text { // This is when we press enter in the text field - (void)tokenField:(VENTokenField *)tokenField didEnterText:(NSString *)text { - [_tokenField reloadData]; + // [_tokenField reloadData]; [self reloadData]; [_tokenField resignFirstResponder]; @@ -260,19 +408,25 @@ - (NSUInteger)numberOfTokensInTokenField:(VENTokenField *)tokenField { } - (void) selectUser: (id) user { + // for 1-1 chat - if(_selectedContacts.count < maximumSelectedUsers || maximumSelectedUsers <= 0) { + // if(_selectedContacts.count < maximumSelectedUsers || maximumSelectedUsers <= 0) { + if ([_selectedContacts containsObject:user]){ + [_selectedContacts removeObject:user]; + } + else{ + [_selectedContacts removeAllObjects]; [_selectedContacts addObject:user]; - - [self.names addObject:user.name]; - - _filterByName = Nil; - [_tokenField reloadData]; - - [self setGroupNameHidden:_selectedContacts.count < 2 || _contactsToExclude.count > 0 duration:0.4]; - - [self reloadData]; } + // [self.names addObject:user.name]; + + _filterByName = Nil; + // [_tokenField reloadData]; + + // [self setGroupNameHidden:_selectedContacts.count < 2 || _contactsToExclude.count > 0 duration:0.4]; + + // [self reloadData]; + // } } @@ -314,17 +468,25 @@ -(void) reloadData { [_contacts addObjectsFromArray: self.overrideContacts()]; } - [_contacts removeObjectsInArray:_selectedContacts]; + // [_contacts removeObjectsInArray:_selectedContacts]; // _contactsToExclude is the users already in the thread - make sure we don't include anyone already in the thread - [_contacts removeObjectsInArray:_contactsToExclude]; - [_contacts sortOnlineThenAlphabetical]; - +// [_contacts removeObjectsInArray:_contactsToExclude]; +// [_contacts sortOnlineThenAlphabetical]; + [_contacts sortAlphabetical]; + if (_filterByName && _filterByName.length) { NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name contains[c] %@", _filterByName]; [_contacts filterUsingPredicate:predicate]; } + //Show empty View + if ([_contacts count] > 0) { + [self showEmptyView:false]; + } + else { + [self showEmptyView:true]; + } [tableView reloadData]; [self updateRightBarButtonActionTitle]; self.navigationItem.rightBarButtonItem.enabled = _selectedContacts.count; @@ -381,7 +543,9 @@ -(void) keyboardWillHide: (NSNotification *) notification { -(void) dismissView { [self dismissViewControllerAnimated:YES completion:Nil]; } - +- (void)backButtonPressed { + [self.navigationController popViewControllerAnimated:true]; +} - (void)updateButtonStatusForInternetConnection { BOOL connected = BChatSDK.connectivity.isConnected; self.navigationItem.rightBarButtonItem.enabled = connected; @@ -390,3 +554,4 @@ - (void)updateButtonStatusForInternetConnection { @end + diff --git a/ChatSDKUI/Classes/Components/View Controllers/BUserCell.h b/ChatSDKUI/Classes/Components/View Controllers/BUserCell.h index 61b12658..30966318 100755 --- a/ChatSDKUI/Classes/Components/View Controllers/BUserCell.h +++ b/ChatSDKUI/Classes/Components/View Controllers/BUserCell.h @@ -23,6 +23,8 @@ -(void) setUser: (id) user; +-(void) setSelectedImage; +-(void) setDeSelectedImage; -(void) setOnline; -(void) setAway; -(void) setOffline; diff --git a/ChatSDKUI/Classes/Components/View Controllers/BUserCell.m b/ChatSDKUI/Classes/Components/View Controllers/BUserCell.m index b97ac615..d2891827 100755 --- a/ChatSDKUI/Classes/Components/View Controllers/BUserCell.m +++ b/ChatSDKUI/Classes/Components/View Controllers/BUserCell.m @@ -7,13 +7,17 @@ // #import "BUserCell.h" - +#import #import @implementation BUserCell - (void)awakeFromNib { [super awakeFromNib]; + self.profileImageView.layer.cornerRadius = self.profileImageView.fh/2.0; + self.profileImageView.layer.borderColor = [UIColor darkGrayColor].CGColor; + self.profileImageView.layer.borderWidth = 1.0; + self.profileImageView.clipsToBounds = YES; } @@ -22,10 +26,10 @@ - (void)setSelected:(BOOL)selected animated:(BOOL)animated { } -(void) setUser: (id) user { - + // _statusImageView.image =  // TODO: Don't use absolute value - self.profileImageView.layer.cornerRadius = 22; - self.profileImageView.clipsToBounds = YES; + // self.profileImageView.layer.cornerRadius = 22; + // self.profileImageView.clipsToBounds = YES; self.selectionStyle = UITableViewCellSelectionStyleNone; self.accessoryType = UITableViewCellAccessoryNone; // If we don't set this then sometimes the cells don't refresh properly @@ -36,20 +40,66 @@ -(void) setUser: (id) user { [self.profileImageView loadAvatar:user]; self.title.text = user.name; - self.subtitle.text = user.statusText; +// self.subtitle.text = user.statusText; +// +// if (user.availability) { +// [self setAvailabilityLabelText:user.availability]; +// } else { +// if (user.online.boolValue) { +// [self setAvailabilityLabelText:[NSBundle t: NSLocalizedString(bOnline, nil)]]; +// } +// else { +// [self setAvailabilityLabelText:[NSBundle t: bOffline]]; +// } +// } +// +// if (user.availability && user.availability.length && user.online.boolValue && ![user.availability isEqualToString:bAvailabilityStateChat]) { +// [self setAway]; +// } else { +// if (user.online.boolValue) { +// [self setOnline]; +// } +// else { +// [self setOffline]; +// } +// } - if (user.availability && user.availability.length && user.online.boolValue) { - [self setAvailability:user.availability]; - } else { - if (user.online.boolValue) { - [self setAvailability:bOnline]; - } - else { - [self setAvailability:bOffline]; - } + self.subtitle.text = @""; + self.statusImageView.hidden = false; + + BOOL value = [[user.meta metaValueForKey:@"can_message"] boolValue]; + if (value == false){ + self.statusImageView.image = [NSBundle uiImageNamed:@"checkbox_disabeled@2x.png"]; + self.subtitle.text = [NSBundle t: NSLocalizedString(@"application_not_installed", nil)]; } +// printf("%@", [user.meta metaStringForKey:@"can_message"]); +// if ([user.meta metaValueForKey:@"can_message"] == true) { +// self.subtitle.text = @"Available"; +// } +// else{ +// self.subtitle.text = @"Not Available"; +// } +// if ((NSNumber*)[user.meta metaValueForKey:@"can_message"] == 1) { +// self.subtitle.text = @"Available"; +// } +// else{ +// self.subtitle.text = @"Not Available"; +// } + + + + // self.subtitle.text = user.statusText; + +// if (user.online.boolValue) { +// [self setOnline]; +// } +// else { +// [self setOffline]; +// } } + + -(void) setAvailabilityLabelText: (NSString *) availability { if(!availability || availability.length == 0) { [self.statusImageView keepVerticallyCentered]; @@ -61,16 +111,17 @@ -(void) setAvailabilityLabelText: (NSString *) availability { self.stateLabel.text = [NSBundle t:availability]; } --(void) setAvailability: (NSString *) availability { - - if ([availability isEqualToString:bAvailabilityStateChat] || [availability isEqualToString:bAvailabilityStateAvailable]) { - availability = bOnline; - } - self.statusImageView.image = [NSBundle uiImageNamed: [NSString stringWithFormat:@"icn_16_status_%@.png", availability]]; +-(void) setSelectedImage { + self.statusImageView.image = [NSBundle uiImageNamed: @"checkBox@2x"]; +} - [self setAvailabilityLabelText:availability]; +-(void) setDeSelectedImage { + self.statusImageView.image = [NSBundle uiImageNamed: @"empty_checkBox@2x.png"]; } + + + -(void) setOnline { self.statusImageView.image = [NSBundle uiImageNamed: @"icn_16_status_online.png"]; } diff --git a/ChatSDKUI/Classes/Components/View Controllers/BUserCell.m.orig b/ChatSDKUI/Classes/Components/View Controllers/BUserCell.m.orig new file mode 100755 index 00000000..3ac1e990 --- /dev/null +++ b/ChatSDKUI/Classes/Components/View Controllers/BUserCell.m.orig @@ -0,0 +1,164 @@ +// +// BUserTableViewCell.m +// Pods +// +// Created by Benjamin Smiley-andrews on 09/08/2016. +// +// + +#import "BUserCell.h" +#import +#import + +@implementation BUserCell + +- (void)awakeFromNib { + [super awakeFromNib]; + self.profileImageView.layer.cornerRadius = self.profileImageView.fh/2.0; + self.profileImageView.layer.borderColor = [UIColor darkGrayColor].CGColor; + self.profileImageView.layer.borderWidth = 1.0; + self.profileImageView.clipsToBounds = YES; + +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated { + [super setSelected:selected animated:animated]; +} + +-(void) setUser: (id) user { + // _statusImageView.image =  + // TODO: Don't use absolute value + // self.profileImageView.layer.cornerRadius = 22; + // self.profileImageView.clipsToBounds = YES; + self.selectionStyle = UITableViewCellSelectionStyleNone; + self.accessoryType = UITableViewCellAccessoryNone; // If we don't set this then sometimes the cells don't refresh properly + + //self.profileImageView.layer.borderWidth = 2; + self.statusImageView.layer.cornerRadius = 6; + [self setAvailabilityLabelText:@""]; + + [self.profileImageView loadAvatar:user]; + + self.title.text = user.name; +// self.subtitle.text = user.statusText; +// +// if (user.availability) { +// [self setAvailabilityLabelText:user.availability]; +// } else { +// if (user.online.boolValue) { +// [self setAvailabilityLabelText:[NSBundle t: NSLocalizedString(bOnline, nil)]]; +// } +// else { +// [self setAvailabilityLabelText:[NSBundle t: bOffline]]; +// } +// } +// +// if (user.availability && user.availability.length && user.online.boolValue && ![user.availability isEqualToString:bAvailabilityStateChat]) { +// [self setAway]; +// } else { +// if (user.online.boolValue) { +// [self setOnline]; +// } +// else { +// [self setOffline]; +// } +// } + +<<<<<<< HEAD + self.subtitle.text = @""; + self.statusImageView.hidden = false; + + BOOL value = [[user.meta metaValueForKey:@"can_message"] boolValue]; + if (value == false){ + self.statusImageView.image = [NSBundle uiImageNamed:@"checkbox_disabeled@2x.png"]; + self.subtitle.text = [NSBundle t: NSLocalizedString(@"application_not_installed", nil)]; + } +// printf("%@", [user.meta metaStringForKey:@"can_message"]); +// if ([user.meta metaValueForKey:@"can_message"] == true) { +// self.subtitle.text = @"Available"; +// } +// else{ +// self.subtitle.text = @"Not Available"; +// } +// if ((NSNumber*)[user.meta metaValueForKey:@"can_message"] == 1) { +// self.subtitle.text = @"Available"; +// } +// else{ +// self.subtitle.text = @"Not Available"; +// } + + + + // self.subtitle.text = user.statusText; + +// if (user.online.boolValue) { +// [self setOnline]; +// } +// else { +// [self setOffline]; +// } +======= + if (user.availability && user.availability.length && user.online.boolValue) { + [self setAvailability:user.availability]; + } else { + if (user.online.boolValue) { + [self setAvailability:bOnline]; + } + else { + [self setAvailability:bOffline]; + } + } +>>>>>>> ba622d8b140a9791dcf57da71313e8792eb58a33 +} + + + +-(void) setAvailabilityLabelText: (NSString *) availability { + if(!availability || availability.length == 0) { + [self.statusImageView keepVerticallyCentered]; + } + else { + self.statusImageView.keepBottomOffsetTo(self.stateLabel).equal = 5; +// self.statusImageView.keepVerticalAlignTo(self.statusImageView.superview).equal = -15; + } + self.stateLabel.text = [NSBundle t:availability]; +} + +<<<<<<< HEAD +-(void) setSelectedImage { + self.statusImageView.image = [NSBundle uiImageNamed: @"checkBox@2x"]; +} + +-(void) setDeSelectedImage { + self.statusImageView.image = [NSBundle uiImageNamed: @"empty_checkBox@2x.png"]; +} + + + + +======= +-(void) setAvailability: (NSString *) availability { + + if ([availability isEqualToString:bAvailabilityStateChat] || [availability isEqualToString:bAvailabilityStateAvailable]) { + availability = bOnline; + } + self.statusImageView.image = [NSBundle uiImageNamed: [NSString stringWithFormat:@"icn_16_status_%@.png", availability]]; + + [self setAvailabilityLabelText:availability]; +} + +>>>>>>> ba622d8b140a9791dcf57da71313e8792eb58a33 +-(void) setOnline { + self.statusImageView.image = [NSBundle uiImageNamed: @"icn_16_status_online.png"]; +} + +-(void) setAway { + self.statusImageView.image = [NSBundle uiImageNamed: @"icn_16_status_away.png"]; +} + +-(void) setOffline { + self.statusImageView.image = [NSBundle uiImageNamed: @"icn_16_status_offline.png"]; +} + + +@end diff --git a/ChatSDKUI/Classes/Components/View Controllers/BUsersViewController.m b/ChatSDKUI/Classes/Components/View Controllers/BUsersViewController.m index 69beaf0f..96ed91e6 100755 --- a/ChatSDKUI/Classes/Components/View Controllers/BUsersViewController.m +++ b/ChatSDKUI/Classes/Components/View Controllers/BUsersViewController.m @@ -11,7 +11,9 @@ #import #import -#define bUserCellIdentifier @"UserCellIdentifier" +#define bUserCellIdentifier @"bUserCellIdentifier" + +//#define bUserCellIdentifier @"UserCellIdentifier" #define bLeaveCellIdentifier @"LeaveCellIdentifier" #define bCell @"BTableCell" @@ -46,23 +48,27 @@ - (void)viewDidLoad { self.title = [NSBundle t:bDetails]; - self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t:bBack] - style:UIBarButtonItemStylePlain - target:self - action:@selector(backButtonPressed)]; +// self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t:bBack] +// style:UIBarButtonItemStylePlain +// target:self +// action:@selector(backButtonPressed)]; - if(_thread.creator.isMe) { - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd - target:self - action:@selector(addUser)]; - } +// if(_thread.creator.isMe) { +// self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd +// target:self +// action:@selector(addUser)]; +// } tableView.separatorColor = [UIColor colorWithRed:200/255.0 green:200/255.0 blue:204/255.0 alpha:1]; - [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:bUserCellIdentifier]; + // [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:bUserCellIdentifier]; [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:bLeaveCellIdentifier]; [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:bCell]; + [tableView registerNib:[UINib nibWithNibName:@"BUserCell" bundle:[NSBundle uiBundle]] forCellReuseIdentifier:bUserCellIdentifier]; + self.tableView.tableFooterView = [[UIView alloc] + initWithFrame:CGRectZero]; + } - (void)viewDidAppear:(BOOL)animated { @@ -89,6 +95,10 @@ - (void)viewDidDisappear:(BOOL)animated { [[NSNotificationCenter defaultCenter] removeObserver:_threadUsersObserver]; } +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return 60; +} + - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == bParticipantsSection) { @@ -102,7 +112,8 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // We only show the add and leave group for private groups - return _thread.type.intValue == bThreadTypePrivateGroup ? bSectionCount : 1; + return 1; + // return _thread.type.intValue == bThreadTypePrivateGroup ? bSectionCount : 1; } - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -117,20 +128,25 @@ - (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:( id user = _users[indexPath.row]; - cell.textLabel.text = user.name; - [cell.imageView loadAvatar:user]; + BUserCell * cell = [tableView_ dequeueReusableCellWithIdentifier:bUserCellIdentifier]; + [cell setUser:user]; - cell.imageView.layer.cornerRadius = 20; - cell.imageView.clipsToBounds = YES; + return cell; - CGSize itemSize = CGSizeMake(40, 40); - - UIGraphicsBeginImageContextWithOptions(itemSize, NO, UIScreen.mainScreen.scale); - CGRect imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height); - [cell.imageView.image drawInRect:imageRect]; - cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext(); - - UIGraphicsEndImageContext(); +// cell.textLabel.text = user.name; +// cell.imageView.image = user && user.imageAsImage ? user.imageAsImage : [NSBundle uiImageNamed: @"icn_user.png"]; +// +// cell.imageView.layer.cornerRadius = 20; +// cell.imageView.clipsToBounds = YES; +// +// CGSize itemSize = CGSizeMake(40, 40); +// +// UIGraphicsBeginImageContextWithOptions(itemSize, NO, UIScreen.mainScreen.scale); +// CGRect imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height); +// [cell.imageView.image drawInRect:imageRect]; +// cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext(); +// +// UIGraphicsEndImageContext(); } else { cell.textLabel.text = [NSBundle t:bNoActiveParticipants]; @@ -167,53 +183,54 @@ -(void) addUser { }, Nil); }]; [((id) nav.topViewController) setRightBarButtonActionTitle:[NSBundle t: bAdd]]; - - [self presentViewController:nav animated:YES completion:Nil]; + [self.navigationController pushViewController:[nav topViewController] animated:YES]; + + // [self presentViewController:nav animated:YES completion:Nil]; } - (void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // The add user button - if (indexPath.section == bParticipantsSection) { - - if (_users.count) { - id user = _users[indexPath.row]; - - // Open the users profile - UIViewController * profileView = [BChatSDK.ui profileViewControllerWithUser:user]; - [self.navigationController pushViewController:profileView animated:YES]; - } - } - if (indexPath.section == bLeaveConvoSection) { - - [BChatSDK.core deleteThread:_thread]; - [BChatSDK.core leaveThread:_thread]; - - [self.navigationController dismissViewControllerAnimated:NO completion:^{ - if (self.parentNavigationController) { - [self.parentNavigationController popViewControllerAnimated:YES]; - } - }]; - } - - [tableView_ deselectRowAtIndexPath:indexPath animated:YES]; +// if (indexPath.section == bParticipantsSection) { +// +// if (_users.count) { +// id user = _users[indexPath.row]; +// +// // Open the users profile +// UIViewController * profileView = [BChatSDK.ui profileViewControllerWithUser:user]; +// [self.navigationController pushViewController:profileView animated:YES]; +// } +// } +// if (indexPath.section == bLeaveConvoSection) { +// +// [BChatSDK.core deleteThread:_thread]; +// [BChatSDK.core leaveThread:_thread]; +// +// [self.navigationController dismissViewControllerAnimated:NO completion:^{ +// if (self.parentNavigationController) { +// [self.parentNavigationController popViewControllerAnimated:YES]; +// } +// }]; +// } +// +// [tableView_ deselectRowAtIndexPath:indexPath animated:YES]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - if (section == bParticipantsSection) { - - if (_thread.type.integerValue & bThreadFilterPrivate) { - return [NSBundle t:bParticipants]; - } - else { - return _thread.users.allObjects.count > 0 ? [NSBundle t:bActiveParticipants] : [NSBundle t:bNoActiveParticipants]; - } - } - if (section == bLeaveConvoSection) { - return @""; - } +// if (section == bParticipantsSection) { +// +// if (_thread.type.integerValue & bThreadFilterPrivate) { +// return [NSBundle t:bParticipants]; +// } +// else { +// return _thread.users.allObjects.count > 0 ? [NSBundle t:bActiveParticipants] : [NSBundle t:bNoActiveParticipants]; +// } +// } +// if (section == bLeaveConvoSection) { +// return @""; +// } return @""; } diff --git a/ChatSDKUI/Classes/Components/View Controllers/BUsersViewController.m.orig b/ChatSDKUI/Classes/Components/View Controllers/BUsersViewController.m.orig new file mode 100755 index 00000000..92f4d379 --- /dev/null +++ b/ChatSDKUI/Classes/Components/View Controllers/BUsersViewController.m.orig @@ -0,0 +1,263 @@ +// +// BUsersViewController.m +// Chat SDK +// +// Created by Simon Smiley-Andrews on 05/11/2014. +// Copyright (c) 2014 deluge. All rights reserved. +// + +#import "BUsersViewController.h" + +#import +#import + +#define bUserCellIdentifier @"bUserCellIdentifier" + +//#define bUserCellIdentifier @"UserCellIdentifier" +#define bLeaveCellIdentifier @"LeaveCellIdentifier" + +#define bCell @"BTableCell" + +#define bParticipantsSection 0 +#define bLeaveConvoSection 1 +#define bSectionCount 2 + +@interface BUsersViewController () + +@end + +@implementation BUsersViewController + +@synthesize tableView; + +-(instancetype) initWithThread: (id) thread { + + self = [super initWithNibName:@"BUsersViewController" bundle:[NSBundle uiBundle]]; + if (self) { + + _users = [NSMutableArray arrayWithArray: thread.users.allObjects]; + [_users removeObject:BChatSDK.currentUser]; + + _thread = thread; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.title = [NSBundle t:bDetails]; + +// self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle t:bBack] +// style:UIBarButtonItemStylePlain +// target:self +// action:@selector(backButtonPressed)]; + +// if(_thread.creator.isMe) { +// self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd +// target:self +// action:@selector(addUser)]; +// } + + tableView.separatorColor = [UIColor colorWithRed:200/255.0 green:200/255.0 blue:204/255.0 alpha:1]; + + // [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:bUserCellIdentifier]; + [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:bLeaveCellIdentifier]; + + [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:bCell]; + [tableView registerNib:[UINib nibWithNibName:@"BUserCell" bundle:[NSBundle uiBundle]] forCellReuseIdentifier:bUserCellIdentifier]; + self.tableView.tableFooterView = [[UIView alloc] + initWithFrame:CGRectZero]; + +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + _internetConnectionHook = [BHook hook:^(NSDictionary * data) { + if(!BChatSDK.connectivity.isConnected) { + [self dismissViewControllerAnimated:YES completion:nil]; + } + }]; + [BChatSDK.hook addHook:_internetConnectionHook withName:bHookInternetConnectivityDidChange]; + + _threadUsersObserver = [[NSNotificationCenter defaultCenter] addObserverForName:bNotificationThreadUsersUpdated object:Nil queue:Nil usingBlock:^(NSNotification * notification) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self reloadData]; + }); + }]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + [BChatSDK.hook removeHook:_internetConnectionHook]; + + [[NSNotificationCenter defaultCenter] removeObserver:_threadUsersObserver]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return 60; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + + if (section == bParticipantsSection) { + return _users.count ? _users.count : 1; + } + if (section == bLeaveConvoSection) { + return 1; + } + return 0; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + // We only show the add and leave group for private groups + return 1; + // return _thread.type.intValue == bThreadTypePrivateGroup ? bSectionCount : 1; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:(NSIndexPath *)indexPath { + + UITableViewCell * cell = [tableView_ dequeueReusableCellWithIdentifier:bCell]; + + cell.textLabel.textColor = [UIColor blackColor]; + + if (indexPath.section == bParticipantsSection) { + + if (_users.count) { + + id user = _users[indexPath.row]; + +<<<<<<< HEAD + BUserCell * cell = [tableView_ dequeueReusableCellWithIdentifier:bUserCellIdentifier]; + [cell setUser:user]; +======= + cell.textLabel.text = user.name; + [cell.imageView loadAvatar:user]; +>>>>>>> ba622d8b140a9791dcf57da71313e8792eb58a33 + + return cell; + +// cell.textLabel.text = user.name; +// cell.imageView.image = user && user.imageAsImage ? user.imageAsImage : [NSBundle uiImageNamed: @"icn_user.png"]; +// +// cell.imageView.layer.cornerRadius = 20; +// cell.imageView.clipsToBounds = YES; +// +// CGSize itemSize = CGSizeMake(40, 40); +// +// UIGraphicsBeginImageContextWithOptions(itemSize, NO, UIScreen.mainScreen.scale); +// CGRect imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height); +// [cell.imageView.image drawInRect:imageRect]; +// cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext(); +// +// UIGraphicsEndImageContext(); + } + else { + cell.textLabel.text = [NSBundle t:bNoActiveParticipants]; + cell.imageView.image = nil; + } + + cell.textLabel.textAlignment = _users.count ? NSTextAlignmentLeft : NSTextAlignmentCenter; + cell.selectionStyle = _users.count ? UITableViewCellSelectionStyleDefault :UITableViewCellSelectionStyleNone; + + return cell; + } + + if (indexPath.section == bLeaveConvoSection) { + + // Reset the image view + cell.imageView.image = nil; + cell.textLabel.text = [NSBundle t:bLeaveConversation]; + cell.textLabel.textColor = [UIColor redColor]; + cell.textLabel.textAlignment = NSTextAlignmentCenter; + } + + return cell; +} + +-(void) addUser { + // Use initWithThread here to make sure we don't show any users already in the thread + // Show the friends view controller + UINavigationController * nav = [BChatSDK.ui friendsNavigationControllerWithUsersToExclude:_thread.users.allObjects onComplete:^(NSArray * users, NSString * groupName){ + [BChatSDK.core addUsers:users toThread:_thread].thenOnMain(^id(id success){ + [UIView alertWithTitle:[NSBundle t:bSuccess] withMessage:[NSBundle t:bAdded]]; + + [self reloadData]; + return Nil; + }, Nil); + }]; + [((id) nav.topViewController) setRightBarButtonActionTitle:[NSBundle t: bAdd]]; + [self.navigationController pushViewController:[nav topViewController] animated:YES]; + + // [self presentViewController:nav animated:YES completion:Nil]; + +} + +- (void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + + // The add user button +// if (indexPath.section == bParticipantsSection) { +// +// if (_users.count) { +// id user = _users[indexPath.row]; +// +// // Open the users profile +// UIViewController * profileView = [BChatSDK.ui profileViewControllerWithUser:user]; +// [self.navigationController pushViewController:profileView animated:YES]; +// } +// } +// if (indexPath.section == bLeaveConvoSection) { +// +// [BChatSDK.core deleteThread:_thread]; +// [BChatSDK.core leaveThread:_thread]; +// +// [self.navigationController dismissViewControllerAnimated:NO completion:^{ +// if (self.parentNavigationController) { +// [self.parentNavigationController popViewControllerAnimated:YES]; +// } +// }]; +// } +// +// [tableView_ deselectRowAtIndexPath:indexPath animated:YES]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + +// if (section == bParticipantsSection) { +// +// if (_thread.type.integerValue & bThreadFilterPrivate) { +// return [NSBundle t:bParticipants]; +// } +// else { +// return _thread.users.allObjects.count > 0 ? [NSBundle t:bActiveParticipants] : [NSBundle t:bNoActiveParticipants]; +// } +// } +// if (section == bLeaveConvoSection) { +// return @""; +// } + return @""; +} + +- (void)reloadData { + + _users = [NSMutableArray arrayWithArray: _thread.users.allObjects]; + [_users removeObject:BChatSDK.currentUser]; + + [self.tableView reloadData]; +} + +#pragma TextField delegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + + [textField resignFirstResponder]; + + return NO; +} + +- (void)backButtonPressed { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/ChatSDKUI/Classes/Components/View Controllers/EmptyChatView.h b/ChatSDKUI/Classes/Components/View Controllers/EmptyChatView.h new file mode 100644 index 00000000..24d4542c --- /dev/null +++ b/ChatSDKUI/Classes/Components/View Controllers/EmptyChatView.h @@ -0,0 +1,20 @@ +// +// EmptyChatView.h +// ChatSDK-ChatUI +// +// Created by Kiran Thakur on 28/06/19. +// + +#import + + +@interface EmptyChatView : UIViewController + +@property (weak, nonatomic) IBOutlet UIImageView *emptyViewImage; +@property (weak, nonatomic) IBOutlet UILabel *titleLabel; +@property (weak, nonatomic) IBOutlet UILabel *subtitleLabel; + +-(void)setText: (NSString *) title setSubTitle: (NSString *) subtitle setEmptyImage: (UIImage*) image; + +@end + diff --git a/ChatSDKUI/Classes/Components/View Controllers/EmptyChatView.m b/ChatSDKUI/Classes/Components/View Controllers/EmptyChatView.m new file mode 100644 index 00000000..a4110cb1 --- /dev/null +++ b/ChatSDKUI/Classes/Components/View Controllers/EmptyChatView.m @@ -0,0 +1,42 @@ +// +// EmptyChatView.m +// ChatSDK-ChatUI +// +// Created by Kiran Thakur on 28/06/19. +// + +#import "EmptyChatView.h" + +@interface EmptyChatView () + + +@end + +@implementation EmptyChatView +@synthesize emptyViewImage; +@synthesize titleLabel; +@synthesize subtitleLabel; + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view from its nib. +} +//- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + +-(void)setText: (NSString *) title setSubTitle: (NSString *) subtitle setEmptyImage: (UIImage*) image { + [titleLabel setText:title]; + [subtitleLabel setText:subtitle]; + [emptyViewImage setImage:image]; +} + +/* +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. +} +*/ + +@end diff --git a/ChatSDKUI/Interface/BUsersViewController.xib b/ChatSDKUI/Interface/BUsersViewController.xib index 9895301e..c9b3fc83 100755 --- a/ChatSDKUI/Interface/BUsersViewController.xib +++ b/ChatSDKUI/Interface/BUsersViewController.xib @@ -1,8 +1,12 @@ - - + + + + + - + + @@ -13,27 +17,27 @@ - + - - - - + + + + - + - + diff --git a/ChatSDKUI/Interface/EmptyChatView.xib b/ChatSDKUI/Interface/EmptyChatView.xib new file mode 100644 index 00000000..179a4d3a --- /dev/null +++ b/ChatSDKUI/Interface/EmptyChatView.xib @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + SFProText-Regular + + + SFProText-Semibold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChatSDKUI/LICENSE b/ChatSDKUI/LICENSE deleted file mode 100755 index 04aa4731..00000000 --- a/ChatSDKUI/LICENSE +++ /dev/null @@ -1,53 +0,0 @@ -Chat SDK License - -We offer a choice of two license for this app. You can either use the [Chat SDK](https://chatsdk.co/chat-sdk-license/) license or the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) license. - -### Chat SDK License Summary - -+ License does not expire. -+ Can be used for creating unlimited applications -+ Can be distributed in binary or object form only -+ Commercial use allowed -+ Can modify source-code but cannot distribute modifications (derivative works) - -### GPLv3 License Summary - -+ Can modify and distribute source code -+ Commerical use allowed -+ Cannot sublicense or hold liable -+ Must include original license -+ Must disclose source - -### FAQ - -1. **I want to release an app to the App Store. Do I have to pay anything?** - - _When you release an app on the app store, you are releasing a compiled app. This is the most common use case for our users. The Chat SDK license allows you to release unlimited commerical apps on the app store without paying anything._ - -2. **Do I have to include any attribution with my binary?** - - _No. Attribution is not required._ - -3. **Do I have to include a license file with my binary?** - - _No. It is not necessary to include a license file with your binary._ - -4. **I'm releasing a commercial app. Do I need to pay anything?** - - _If you are releasing the app in binary form, then commercial use is allowed and you do not need to pay anything._ - -5. **Can I release the Chat SDK straight onto the App Store with no modifications?** - - _Yes. You can release the code in binary form with or without modifications._ - -6. **Do I need to get any kind of permission from you before releasing my app?** - - _No. It is not necessary to ask permission before releasing your app under the Chat SDK license._ - -7. **I have an open source project, can I include the Chat SDK?** - - _If your project license is compatible with the GPLv3 license then you can. If not, you should email us and we will come up with a custom licensing scheme. The price will depend on the circumstances. We will often allow the code to be used for free for non-commerical projects._ - -8. **Why do you have a dual licensing scheme?** - - _We want to give our users as much flexibility as possible while retaining some degree of control over our code. Imageing that someone decided to make some minor modificaitons and started selling our code on a marketplace. This wouldn't benefit anyone because we want to make the code available for free. The dual licensing scheme allows you to do pretty much anything apart from sell our source code directly._ \ No newline at end of file diff --git a/Xcode/ChatSDK Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Xcode/ChatSDK Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Xcode/ChatSDK Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + +