diff --git a/Vector.xcodeproj/project.pbxproj b/Vector.xcodeproj/project.pbxproj index cc2e007a1..cd75b7320 100644 --- a/Vector.xcodeproj/project.pbxproj +++ b/Vector.xcodeproj/project.pbxproj @@ -136,6 +136,9 @@ F056418C1C7CBEBD002276ED /* group.png in Resources */ = {isa = PBXBuildFile; fileRef = F05641891C7CBEBD002276ED /* group.png */; }; F056418D1C7CBEBD002276ED /* group@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F056418A1C7CBEBD002276ED /* group@2x.png */; }; F056418E1C7CBEBD002276ED /* group@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F056418B1C7CBEBD002276ED /* group@3x.png */; }; + F05641921C7DF9DE002276ED /* error.png in Resources */ = {isa = PBXBuildFile; fileRef = F056418F1C7DF9DE002276ED /* error.png */; }; + F05641931C7DF9DE002276ED /* error@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F05641901C7DF9DE002276ED /* error@2x.png */; }; + F05641941C7DF9DE002276ED /* error@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F05641911C7DF9DE002276ED /* error@3x.png */; }; F05895001B8B7E6600B73E85 /* RoomBubbleCellData.m in Sources */ = {isa = PBXBuildFile; fileRef = F05894FF1B8B7E6600B73E85 /* RoomBubbleCellData.m */; }; F08BE09E1B87025B00C480FB /* EventFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = F08BE09D1B87025B00C480FB /* EventFormatter.m */; }; F08BE0A21B87064000C480FB /* RoomDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = F08BE0A11B87064000C480FB /* RoomDataSource.m */; }; @@ -386,6 +389,9 @@ F05641891C7CBEBD002276ED /* group.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = group.png; sourceTree = ""; }; F056418A1C7CBEBD002276ED /* group@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "group@2x.png"; sourceTree = ""; }; F056418B1C7CBEBD002276ED /* group@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "group@3x.png"; sourceTree = ""; }; + F056418F1C7DF9DE002276ED /* error.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = error.png; sourceTree = ""; }; + F05641901C7DF9DE002276ED /* error@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "error@2x.png"; sourceTree = ""; }; + F05641911C7DF9DE002276ED /* error@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "error@3x.png"; sourceTree = ""; }; F05894FE1B8B7E6600B73E85 /* RoomBubbleCellData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomBubbleCellData.h; sourceTree = ""; }; F05894FF1B8B7E6600B73E85 /* RoomBubbleCellData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomBubbleCellData.m; sourceTree = ""; }; F08BE09C1B87025B00C480FB /* EventFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EventFormatter.h; sourceTree = ""; }; @@ -950,6 +956,9 @@ F0DD7D1B1B7AA8C900C4BE02 /* Images */ = { isa = PBXGroup; children = ( + F056418F1C7DF9DE002276ED /* error.png */, + F05641901C7DF9DE002276ED /* error@2x.png */, + F05641911C7DF9DE002276ED /* error@3x.png */, F05641891C7CBEBD002276ED /* group.png */, F056418A1C7CBEBD002276ED /* group@2x.png */, F056418B1C7CBEBD002276ED /* group@3x.png */, @@ -1133,6 +1142,7 @@ F001D7631B8207C000A162C3 /* RoomInputToolbarView.xib in Resources */, F02528DE1C11B6FC00E1FE1B /* camera_switch@3x.png in Resources */, F02529091C11B6FC00E1FE1B /* upload_icon.png in Resources */, + F05641931C7DF9DE002276ED /* error@2x.png in Resources */, F0D2D9861C197DCB007B8C96 /* RoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, F02528F21C11B6FC00E1FE1B /* remove_icon.png in Resources */, F02528F51C11B6FC00E1FE1B /* remove.png in Resources */, @@ -1176,6 +1186,7 @@ F09EE0031C5134BE0078712F /* RoomIncomingTextMsgWithoutSenderNameBubbleCell.xib in Resources */, F02528E81C11B6FC00E1FE1B /* logo@2x.png in Resources */, F0D2D98A1C197DCB007B8C96 /* RoomIncomingTextMsgWithoutSenderInfoBubbleCell.xib in Resources */, + F05641941C7DF9DE002276ED /* error@3x.png in Resources */, F02528E21C11B6FC00E1FE1B /* create_room@3x.png in Resources */, F02528E11C11B6FC00E1FE1B /* create_room@2x.png in Resources */, F02528EC1C11B6FC00E1FE1B /* mute_icon.png in Resources */, @@ -1214,6 +1225,7 @@ F0DDDBB91C5A5F55000C6C46 /* Icon-170@3x.png in Resources */, F003AA7C1C68A1F6008B430C /* ExpandedRoomTitleView.xib in Resources */, F02528D71C11B6FC00E1FE1B /* camera_capture@3x.png in Resources */, + F05641921C7DF9DE002276ED /* error.png in Resources */, F02528EF1C11B6FC00E1FE1B /* placeholder.png in Resources */, F094A9B41B78D8F000B1FBBF /* Main.storyboard in Resources */, 71F7F51E1C23079F00E7ED8F /* ContactTableViewCell.xib in Resources */, diff --git a/Vector/Assets/Images/error.png b/Vector/Assets/Images/error.png new file mode 100644 index 000000000..949063aa5 Binary files /dev/null and b/Vector/Assets/Images/error.png differ diff --git a/Vector/Assets/Images/error@2x.png b/Vector/Assets/Images/error@2x.png new file mode 100644 index 000000000..7e0484dbb Binary files /dev/null and b/Vector/Assets/Images/error@2x.png differ diff --git a/Vector/Assets/Images/error@3x.png b/Vector/Assets/Images/error@3x.png new file mode 100644 index 000000000..e030aa759 Binary files /dev/null and b/Vector/Assets/Images/error@3x.png differ diff --git a/Vector/Assets/en.lproj/Vector.strings b/Vector/Assets/en.lproj/Vector.strings index f4197aa0c..6f11a3c38 100644 --- a/Vector/Assets/en.lproj/Vector.strings +++ b/Vector/Assets/en.lproj/Vector.strings @@ -118,6 +118,9 @@ "room_two_users_are_typing" = "%@ & %@ are typing..."; "room_many_users_are_typing" = "%@, %@ & others are typing..."; "room_message_placeholder" = "Type a message..."; +"room_offline_notification" = "Connectivity to the server has been lost."; +"room_unsent_messages_notification" = "Messages not sent."; +"room_prompt_resent" = "Resend now?"; "room_event_action_edit" = "Edit"; "room_event_action_copy" = "Copy"; "room_event_action_share" = "Share"; diff --git a/Vector/Utils/EventFormatter.m b/Vector/Utils/EventFormatter.m index 5ff193280..fc3d31a2c 100644 --- a/Vector/Utils/EventFormatter.m +++ b/Vector/Utils/EventFormatter.m @@ -50,7 +50,7 @@ self.prefixTextColor = kVectorTextColorGray; self.bingTextColor = kVectorColorGreen; self.sendingTextColor = kVectorTextColorGray; - self.errorTextColor = [UIColor redColor]; + self.errorTextColor = kVectorTextColorRed; self.defaultTextFont = [UIFont systemFontOfSize:15]; self.prefixTextFont = [UIFont boldSystemFontOfSize:15]; diff --git a/Vector/ViewController/RoomViewController.m b/Vector/ViewController/RoomViewController.m index ac44717ba..d68a3f6ee 100644 --- a/Vector/ViewController/RoomViewController.m +++ b/Vector/ViewController/RoomViewController.m @@ -232,12 +232,18 @@ [super viewWillAppear:animated]; [self listenTypingNotifications]; + + // Observe network reachability + [[AppDelegate theDelegate] addObserver:self forKeyPath:@"isOffline" options:0 context:nil]; + [self refreshActivitiesViewDisplay]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; + [[AppDelegate theDelegate] removeObserver:self forKeyPath:@"isOffline"]; + // hide action if (self.currentAlert) { @@ -1089,7 +1095,7 @@ } } -#pragma mark - typing management +#pragma mark - Typing management - (void)removeTypingNotificationsListener { @@ -1122,69 +1128,177 @@ { [typingUsers removeObjectAtIndex:index]; } + // Ignore this notification if both arrays are empty if (currentTypingUsers.count || typingUsers.count) { currentTypingUsers = typingUsers; - [self refreshTypingView]; + [self refreshActivitiesViewDisplay]; } } }]; currentTypingUsers = self.roomDataSource.room.typingUsers; - [self refreshTypingView]; + [self refreshActivitiesViewDisplay]; } } -- (void)refreshTypingView +- (void)refreshTypingNotification { - NSString* text = nil; - NSUInteger count = currentTypingUsers.count; - - // get the room member names - NSMutableArray *names = [[NSMutableArray alloc] init]; - - // keeps the only the first two users - for(int i = 0; i < MIN(count, 2); i++) + if (self.activitiesView) { - NSString* name = [currentTypingUsers objectAtIndex:i]; + // Prepare here typing notification + NSString* text = nil; + NSUInteger count = currentTypingUsers.count; - MXRoomMember* member = [self.roomDataSource.room.state memberWithUserId:name]; + // get the room member names + NSMutableArray *names = [[NSMutableArray alloc] init]; - if (member && member.displayname.length) + // keeps the only the first two users + for(int i = 0; i < MIN(count, 2); i++) { - name = member.displayname; + NSString* name = [currentTypingUsers objectAtIndex:i]; + + MXRoomMember* member = [self.roomDataSource.room.state memberWithUserId:name]; + + if (member && member.displayname.length) + { + name = member.displayname; + } + + // sanity check + if (name) + { + [names addObject:name]; + } } - // sanity check - if (name) + if (0 == names.count) { - [names addObject:name]; + // something to do ? + } + else if (1 == names.count) + { + text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_one_user_is_typing", @"Vector", nil), [names objectAtIndex:0]]; + } + else if (2 == names.count) + { + text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_two_users_are_typing", @"Vector", nil), [names objectAtIndex:0], [names objectAtIndex:1]]; + } + else + { + text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_many_users_are_typing", @"Vector", nil), [names objectAtIndex:0], [names objectAtIndex:1]]; + } + + [((RoomActivitiesView*) self.activitiesView) displayTypingNotification:text]; + } +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([@"isOffline" isEqualToString:keyPath]) + { + [self refreshActivitiesViewDisplay]; + } +} + +#pragma mark - Unreachable Network Handling + +- (void)refreshActivitiesViewDisplay +{ + if (self.activitiesView) + { + if ([AppDelegate theDelegate].isOffline) + { + [((RoomActivitiesView*) self.activitiesView) displayNetworkErrorNotification:NSLocalizedStringFromTable(@"room_offline_notification", @"Vector", nil)]; + } + else if ([self checkUnsentMessages] == NO) + { + [self refreshTypingNotification]; } } - - if (0 == names.count) - { - // something to do ? - } - else if (1 == names.count) - { - text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_one_user_is_typing", @"Vector", nil), [names objectAtIndex:0]]; - } - else if (2 == names.count) - { - text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_two_users_are_typing", @"Vector", nil), [names objectAtIndex:0], [names objectAtIndex:1]]; - } - else - { - text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_many_users_are_typing", @"Vector", nil), [names objectAtIndex:0], [names objectAtIndex:1]]; - } +} + + +#pragma mark - Unsent Messages Handling + +-(BOOL)checkUnsentMessages +{ + BOOL hasUnsent = NO; if (self.activitiesView) { - [((RoomActivitiesView*) self.activitiesView) updateTypingMessage:text]; + NSArray *outgoingMsgs = self.roomDataSource.room.outgoingMessages; + + for (MXEvent *event in outgoingMsgs) + { + if (event.mxkState == MXKEventStateSendingFailed) + { + hasUnsent = YES; + break; + } + } + + if (hasUnsent) + { + NSString *firstComponent = NSLocalizedStringFromTable(@"room_unsent_messages_notification", @"Vector", nil); + NSString *secondComponent = NSLocalizedStringFromTable(@"room_prompt_resent", @"Vector", nil); + + NSMutableAttributedString *notification = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ %@", firstComponent, secondComponent]]; + + NSRange range = NSMakeRange(firstComponent.length + 1, secondComponent.length); + [notification addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:range]; + + [((RoomActivitiesView*) self.activitiesView) displayUnsentMessagesNotification:notification onLabelTapGesture:^{ + + // List unsent event ids + NSArray *outgoingMsgs = self.roomDataSource.room.outgoingMessages; + NSMutableArray *failedEventIds = [NSMutableArray arrayWithCapacity:outgoingMsgs.count]; + + for (MXEvent *event in outgoingMsgs) + { + if (event.mxkState == MXKEventStateSendingFailed) + { + [failedEventIds addObject:event.eventId]; + } + } + + // Launch iterative operation + [self resendFailedEvent:0 inArray:failedEventIds]; + + }]; + } } + + return hasUnsent; +} + +- (void)resendFailedEvent:(NSUInteger)index inArray:(NSArray*)failedEventIds +{ + if (index < failedEventIds.count) + { + NSString *failedEventId = failedEventIds[index]; + NSUInteger nextIndex = index + 1; + + // Let the datasource resend. It will manage local echo, etc. + [self.roomDataSource resendEventWithEventId:failedEventId success:^(NSString *eventId) { + + [self resendFailedEvent:nextIndex inArray:failedEventIds]; + + } failure:^(NSError *error) { + + [self resendFailedEvent:nextIndex inArray:failedEventIds]; + + }]; + + return; + } + + // Refresh activities view + [self refreshActivitiesViewDisplay]; } @end diff --git a/Vector/Views/RoomActivitiesView/RoomActivitiesView.h b/Vector/Views/RoomActivitiesView/RoomActivitiesView.h index b027e6406..502f1dac6 100644 --- a/Vector/Views/RoomActivitiesView/RoomActivitiesView.h +++ b/Vector/Views/RoomActivitiesView/RoomActivitiesView.h @@ -20,16 +20,42 @@ /** `RoomExtraInfosInfoView` instance is a view used to display extra information */ -@interface RoomActivitiesView : MXKRoomActivitiesView +@interface RoomActivitiesView : MXKRoomActivitiesView @property (weak, nonatomic) IBOutlet UIView *separatorView; -@property (weak, nonatomic) IBOutlet UIImageView *typingImageView; +@property (weak, nonatomic) IBOutlet UIImageView *iconImageView; @property (weak, nonatomic) IBOutlet UILabel *messageLabel; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *mainHeightConstraint; +/** + Notify that some messages are not sent. + Replace the current notification if any. + + @param labelText the notification message + @param onLabelTapGesture block called when user taps on label. + */ +- (void)displayUnsentMessagesNotification:(NSAttributedString*)labelText onLabelTapGesture:(void (^)(void))onLabelTapGesture; + +/** + Display network error. + Replace the current notification if any. + + @param labelText the notification message + */ +- (void)displayNetworkErrorNotification:(NSString*)labelText; + +/** + Display a typing notification. + Replace the current notification if any. + + @param labelText the current typing message. + */ +- (void)displayTypingNotification:(NSString*)labelText; + +/** + Remove any displayed information. + */ +- (void)reset; -// update the displayed typing message. -// nil message hides the typing icon too. -- (void)updateTypingMessage:(NSString*)message; @end diff --git a/Vector/Views/RoomActivitiesView/RoomActivitiesView.m b/Vector/Views/RoomActivitiesView/RoomActivitiesView.m index 8cec309f3..b34b8a060 100644 --- a/Vector/Views/RoomActivitiesView/RoomActivitiesView.m +++ b/Vector/Views/RoomActivitiesView/RoomActivitiesView.m @@ -18,6 +18,8 @@ #import "VectorDesignValues.h" +#import + @implementation RoomActivitiesView + (UINib *)nib @@ -37,27 +39,89 @@ self.separatorView.backgroundColor = kVectorColorLightGrey; self.messageLabel.textColor = kVectorTextColorGray; - -// self.typingImageView.backgroundColor = kVectorColorGreen; -// self.typingImageView.layer.cornerRadius = self.typingImageView.frame.size.height / 2; - } -// update the displayed typing message. -// nil message hides the typing icon too. -- (void)updateTypingMessage:(NSString*)message +- (void)displayUnsentMessagesNotification:(NSAttributedString*)labelText onLabelTapGesture:(void (^)(void))onLabelTapGesture { - if (message) + [self reset]; + + if (labelText.length) { - self.typingImageView.hidden = false; - self.messageLabel.hidden = false; - self.messageLabel.text = message; + self.iconImageView.image = [UIImage imageNamed:@"error"]; + self.messageLabel.attributedText = labelText; + self.messageLabel.textColor = kVectorTextColorRed; + + self.iconImageView.hidden = NO; + self.messageLabel.hidden = NO; + + if (onLabelTapGesture) + { + objc_setAssociatedObject(self.messageLabel, "onLabelTapGesture", [onLabelTapGesture copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // Listen to label tap + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onLabelTap:)]; + [tapGesture setNumberOfTouchesRequired:1]; + [tapGesture setNumberOfTapsRequired:1]; + [tapGesture setDelegate:self]; + [self.messageLabel addGestureRecognizer:tapGesture]; + self.messageLabel.userInteractionEnabled = YES; + } } - else +} + +- (void)onLabelTap:(UITapGestureRecognizer*)sender +{ + void (^onLabelTapGesture)(void) = objc_getAssociatedObject(self.messageLabel, "onLabelTapGesture"); + if (onLabelTapGesture) { - self.typingImageView.hidden = true; - self.messageLabel.hidden = true; + onLabelTapGesture (); } } +- (void)displayNetworkErrorNotification:(NSString*)labelText +{ + [self reset]; + + if (labelText.length) + { + self.iconImageView.image = [UIImage imageNamed:@"error"]; + self.messageLabel.text = labelText; + self.messageLabel.textColor = kVectorTextColorRed; + + self.iconImageView.hidden = NO; + self.messageLabel.hidden = NO; + } +} + +- (void)displayTypingNotification:(NSString*)labelText +{ + [self reset]; + + if (labelText.length) + { + self.iconImageView.image = [UIImage imageNamed:@"typing"]; + self.messageLabel.text = labelText; + + self.iconImageView.hidden = NO; + self.messageLabel.hidden = NO; + } +} + +- (void)reset +{ + self.iconImageView.hidden = YES; + self.messageLabel.hidden = YES; + + self.messageLabel.textColor = kVectorTextColorGray; + + // Remove all gesture recognizer + while (self.messageLabel.gestureRecognizers.count) + { + [self.messageLabel removeGestureRecognizer:self.messageLabel.gestureRecognizers[0]]; + } + self.messageLabel.userInteractionEnabled = NO; + + objc_removeAssociatedObjects(self.messageLabel); +} + @end diff --git a/Vector/Views/RoomActivitiesView/RoomActivitiesView.xib b/Vector/Views/RoomActivitiesView/RoomActivitiesView.xib index 89f4ba039..16ff89abd 100644 --- a/Vector/Views/RoomActivitiesView/RoomActivitiesView.xib +++ b/Vector/Views/RoomActivitiesView/RoomActivitiesView.xib @@ -1,8 +1,8 @@ - + - + @@ -63,10 +63,10 @@ + -