From 2c8a87f8bcc62aa39993e86597a5102ca27a8576 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 28 Jan 2022 12:07:08 +0100 Subject: [PATCH] =?UTF-8?q?Add=20a=20floating=20=E2=80=9Cscroll=20to=20bot?= =?UTF-8?q?tom=E2=80=9D=20button=20in=20the=20corner=20of=20chat=20convers?= =?UTF-8?q?ation=20when=20scrolling=20up.=20Also,=20instead=20of=20always?= =?UTF-8?q?=20scrolling=20to=20the=20bottom=20of=20the=20chat=20conversati?= =?UTF-8?q?on=20when=20receiving=20a=20new=20message,=20a=20=E2=80=9Cunrea?= =?UTF-8?q?d=20message=20count=E2=80=9D=20badge=20is=20added=20on=20the=20?= =?UTF-8?q?scroll=20down=20button=20to=20notify=20that=20new=20messages=20?= =?UTF-8?q?are=20available.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Classes/ChatConversationTableView.h | 2 + Classes/ChatConversationTableView.m | 12 ++- Classes/ChatConversationView.m | 13 ++- Classes/FloatingScrollDownButton.swift | 99 ++++++++++++++++++ Classes/linphone-Bridging-Header.h | 2 +- Resources/images/scroll_to_bottom_default.png | Bin 0 -> 15058 bytes linphone.xcodeproj/project.pbxproj | 40 ++++--- 7 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 Classes/FloatingScrollDownButton.swift create mode 100644 Resources/images/scroll_to_bottom_default.png diff --git a/Classes/ChatConversationTableView.h b/Classes/ChatConversationTableView.h index 1fe9ff3e2..08a7a6d70 100644 --- a/Classes/ChatConversationTableView.h +++ b/Classes/ChatConversationTableView.h @@ -58,6 +58,8 @@ @property(nonatomic, strong) id chatRoomDelegate; @property NSMutableDictionary *imagesInChatroom; @property(nonatomic) NSTimer *ephemeralDisplayTimer; +@property (nullable, nonatomic) UIButton *floatingScrollButton; +@property (nullable, nonatomic) UILabel *scrollBadge; - (void)addEventEntry:(LinphoneEventLog *)event; - (void)scrollToBottom:(BOOL)animated; diff --git a/Classes/ChatConversationTableView.m b/Classes/ChatConversationTableView.m index d5b827a54..199c188d2 100644 --- a/Classes/ChatConversationTableView.m +++ b/Classes/ChatConversationTableView.m @@ -53,6 +53,7 @@ [self stopEphemeralDisplayTimer]; [NSNotificationCenter.defaultCenter removeObserver:self]; [super viewWillDisappear:animated]; + [_floatingScrollButton setHidden:TRUE]; } #pragma mark - @@ -83,6 +84,7 @@ bool oneToOne = capabilities & LinphoneChatRoomCapabilitiesOneToOne; bctbx_list_t *chatRoomEvents = linphone_chat_room_get_history_events(_chatRoom, 0); + int unread_count = 0; bctbx_list_t *head = chatRoomEvents; size_t listSize = bctbx_list_size(chatRoomEvents); @@ -95,6 +97,11 @@ chatRoomEvents = chatRoomEvents->next; } else { LinphoneChatMessage *chat = linphone_event_log_get_chat_message(event); + if (chat && !linphone_chat_message_is_read(chat)) { + if (unread_count == 0) { + // [eventList addObject:[NSString stringWithString:@"Ceci est un test wesh wesh"]]; + } + } // if auto_download is available and file transfer in progress, not add event now if (!(autoDownload && chat && linphone_chat_message_is_file_transfer_in_progress(chat))) { [totalEventList addObject:[NSValue valueWithPointer:linphone_event_log_ref(event)]]; @@ -164,6 +171,8 @@ [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(count - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES]; + if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) + [ChatConversationView markAsRead:_chatRoom]; } - (void)scrollToLastUnread:(BOOL)animated { @@ -411,7 +420,7 @@ static const CGFloat MESSAGE_SPACING_PERCENTAGE = 1.f; handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) { LinphoneChatMessage *msg = linphone_event_log_get_chat_message(event); [VIEW(ChatConversationView) initiateReplyViewForMessage:msg]; - + [self scrollToBottom:TRUE]; }]; UISwipeActionsConfiguration *swipeActionConfig = [UISwipeActionsConfiguration configurationWithActions:@[replyAction]]; @@ -509,4 +518,5 @@ static const CGFloat MESSAGE_SPACING_PERCENTAGE = 1.f; [self reloadData]; } + @end diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index ad61bfa3f..050d43c88 100644 --- a/Classes/ChatConversationView.m +++ b/Classes/ChatConversationView.m @@ -631,7 +631,6 @@ static UICompositeViewDescription *compositeDescription = nil; _composeIndicatorView.frame = newComposingFrame; } completion:^(BOOL finished) { - [_tableController scrollToBottom:TRUE]; _composeIndicatorView.hidden = !visible; }]; } @@ -1348,10 +1347,18 @@ void on_chat_room_chat_message_received(LinphoneChatRoom *cr, const LinphoneEven const LinphoneAddress *from = linphone_chat_message_get_from_address(chat); if (!from) return; - + + bool isDisplayingBottomOfTable = [view.tableController.tableView indexPathsForVisibleRows].lastObject.row == [view.tableController totalNumberOfItems] - 1; [view.tableController addEventEntry:(LinphoneEventLog *)event_log]; [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneMessageReceived object:view]; - [view.tableController scrollToLastUnread:TRUE]; + + + if (isDisplayingBottomOfTable) { + [view.tableController scrollToBottom:TRUE]; + } else { + int unread_msg = linphone_chat_room_get_unread_messages_count(cr); + [[view.tableController scrollBadge] setText:[NSString stringWithFormat:@"%d", unread_msg]]; + } } void on_chat_room_chat_message_sending(LinphoneChatRoom *cr, const LinphoneEventLog *event_log) { diff --git a/Classes/FloatingScrollDownButton.swift b/Classes/FloatingScrollDownButton.swift new file mode 100644 index 000000000..9b6089e07 --- /dev/null +++ b/Classes/FloatingScrollDownButton.swift @@ -0,0 +1,99 @@ +// +// FloatingScrollDownButton.swift +// linphone +// +// Created by QuentinArguillere on 27/01/2022. +// + +import Foundation +import UIKit + +public extension ChatConversationTableView { + + private enum Constants { + static let trailingValue: CGFloat = 20.0 + static let leadingValue: CGFloat = 85.0 + static let buttonHeight: CGFloat = 40.0 + static let buttonWidth: CGFloat = 40.0 + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.tableFooterView = UIView() + createFloatingButton() + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + if let lastCellRowIndex = tableView.indexPathsForVisibleRows?.last?.row { + if( lastCellRowIndex != self.totalNumberOfItems() - 1) { + self.floatingScrollButton?.isHidden = false + self.scrollBadge?.isHidden = (self.scrollBadge?.text == nil) + } else { + self.floatingScrollButton?.isHidden = true + self.scrollBadge?.text = nil + } + } + } + + private func createFloatingButton() { + self.floatingScrollButton = UIButton(type: .custom) + self.floatingScrollButton?.translatesAutoresizingMaskIntoConstraints = false + constrainFloatingButtonToWindow() + self.floatingScrollButton?.setImage(UIImage(named: "scroll_to_bottom_default"), for: .normal) + self.floatingScrollButton?.addTarget(self, action: #selector(scrollToBottomButtonAction(_:)), for: .touchUpInside) + self.floatingScrollButton?.isHidden = true; + addBadgeToButon(badge: nil) + } + + private func constrainFloatingButtonToWindow() { + DispatchQueue.main.async { + guard let keyWindow = UIApplication.shared.keyWindow, + let floatingButton = self.floatingScrollButton else { return } + keyWindow.addSubview(floatingButton) + keyWindow.trailingAnchor.constraint(equalTo: floatingButton.trailingAnchor, + constant: Constants.trailingValue).isActive = true + keyWindow.bottomAnchor.constraint(equalTo: floatingButton.bottomAnchor, + constant: Constants.leadingValue).isActive = true + floatingButton.widthAnchor.constraint(equalToConstant: + Constants.buttonWidth).isActive = true + floatingButton.heightAnchor.constraint(equalToConstant: + Constants.buttonHeight).isActive = true + } + } + + @IBAction private func scrollToBottomButtonAction(_ sender: Any) { + scroll(toBottom: true) + } + + + private func addBadgeToButon(badge: String?) { + self.scrollBadge = UILabel() + self.scrollBadge?.text = badge + self.scrollBadge?.textColor = UIColor.white + self.scrollBadge?.backgroundColor = UIColor.red + self.scrollBadge?.font = UIFont.systemFont(ofSize: 12.0) + self.scrollBadge?.sizeToFit() + self.scrollBadge?.textAlignment = .center + + if let badgeSize = self.scrollBadge?.frame.size, let scrollButton = self.floatingScrollButton { + let height = max(18, Double(badgeSize.height) + 5.0) + let width = max(height, Double(badgeSize.width) + 10.0) + + var vertical: Double?, horizontal: Double? + let badgeInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 15) + + vertical = Double(badgeInset.top) - Double(badgeInset.bottom) + horizontal = Double(badgeInset.left) - Double(badgeInset.right) + + let x = (Double(scrollButton.bounds.size.width) - 10 + horizontal!) + let y = -(Double(badgeSize.height) / 2) - 10 + vertical! + self.scrollBadge?.frame = CGRect(x: x, y: y, width: width, height: height) + + self.scrollBadge!.layer.cornerRadius = self.scrollBadge!.frame.height/2 + self.scrollBadge!.layer.masksToBounds = true + scrollButton.addSubview(self.scrollBadge!) + } + } +} + diff --git a/Classes/linphone-Bridging-Header.h b/Classes/linphone-Bridging-Header.h index a520acc8e..3e52d89b6 100644 --- a/Classes/linphone-Bridging-Header.h +++ b/Classes/linphone-Bridging-Header.h @@ -8,4 +8,4 @@ #import "FastAddressBook.h" #import "Log.h" #import "AudioHelper.h" - +#import "ChatConversationTableView.h" diff --git a/Resources/images/scroll_to_bottom_default.png b/Resources/images/scroll_to_bottom_default.png new file mode 100644 index 0000000000000000000000000000000000000000..3be410ae19403a8e7c8f51cdddb080cae1ff202f GIT binary patch literal 15058 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