diff --git a/Riot/Modules/Common/PresenceIndicator/PresenceIndicatorListener.swift b/Riot/Modules/Common/PresenceIndicator/PresenceIndicatorListener.swift new file mode 100644 index 000000000..8f11946a4 --- /dev/null +++ b/Riot/Modules/Common/PresenceIndicator/PresenceIndicatorListener.swift @@ -0,0 +1,63 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Dedicated listener object for a user Presence status. +class PresenceIndicatorListener { + // MARK: - Properties + private let userId: String + private var presence: MXPresence + private let onUpdate: (MXPresence) -> Void + private var presenceObserver: Any? + + // MARK: - Setup + /// Init. + /// + /// - Parameters: + /// - userId: the user id + /// - presence: initial presence of the user + /// - onUpdate: callback for Presence updates + init(userId: String, presence: MXPresence, onUpdate: @escaping (MXPresence) -> Void) { + self.userId = userId + self.presence = presence + self.onUpdate = onUpdate + + NotificationCenter.default.addObserver(forName: .mxkContactManagerMatrixUserPresenceChange, + object: nil, + queue: .main) { [weak self] notif in + guard let self = self, + self.userId == notif.object as? String, + let presenceString = notif.userInfo?[kMXKContactManagerMatrixPresenceKey] as? String else { + return + } + + let newPresence = MXTools.presence(presenceString) + + guard self.presence != newPresence else { return } + + self.presence = newPresence + self.onUpdate(newPresence) + } + } + + deinit { + if let presenceObserver = presenceObserver { + NotificationCenter.default.removeObserver(presenceObserver) + self.presenceObserver = nil + } + } +} diff --git a/Riot/Modules/Common/PresenceIndicator/PresenceIndicatorView.swift b/Riot/Modules/Common/PresenceIndicator/PresenceIndicatorView.swift index aff0a3a89..c6c8de7b6 100644 --- a/Riot/Modules/Common/PresenceIndicator/PresenceIndicatorView.swift +++ b/Riot/Modules/Common/PresenceIndicator/PresenceIndicatorView.swift @@ -16,6 +16,11 @@ import UIKit +/// Delegate for `PresenceIndicatorView`. +@objc protocol PresenceIndicatorViewDelegate: AnyObject { + func presenceIndicatorViewDidUpdateVisibility(_ presenceIndicatorView: PresenceIndicatorView, isHidden: Bool) +} + /// `PresenceIndicatorView` is used to display a presence indicator over an avatar. @objcMembers @IBDesignable @@ -24,9 +29,24 @@ final class PresenceIndicatorView: UIView { @IBInspectable var borderWidth: CGFloat = 0.0 var borderColor: UIColor = ThemeService.shared().theme.backgroundColor - // MARK: - Private Properties + // MARK: - Properties + + // MARK: Private private let borderLayer = CALayer() - + private var listener: PresenceIndicatorListener? + + // MARK: Internal + weak var delegate: PresenceIndicatorViewDelegate? + + // MARK: Override + override var isHidden: Bool { + didSet { + if oldValue != isHidden, let delegate = delegate { + delegate.presenceIndicatorViewDidUpdateVisibility(self, isHidden: isHidden) + } + } + } + // MARK: - Private Constants private enum Constants { static let borderLayerOffset: CGFloat = 1.0 @@ -57,6 +77,34 @@ final class PresenceIndicatorView: UIView { } // MARK: - Internal Methods + /// Configures the view and starts listening Presence updates for given user. + /// + /// - Parameters: + /// - userId: the user id + /// - presence: the initial Presence of the user + func configure(userId: String, presence: MXPresence) { + setPresence(presence) + self.listener = PresenceIndicatorListener(userId: userId, + presence: presence) { [weak self] presence in + guard let self = self else { return } + self.setPresence(presence) + } + } + + /// Stop listening to Presence updates and hides the indicator. + /// This should be called before reuse or if current room moves from direct to non-direct. + func stopListeningPresenceUpdates() { + self.listener = nil + self.isHidden = true + } +} + +// MARK: - Private Methods +private extension PresenceIndicatorView { + func setup() { + self.layer.addSublayer(borderLayer) + } + /// Updates presence indicator with given `MXPresence`. /// /// - Parameters: @@ -66,22 +114,18 @@ final class PresenceIndicatorView: UIView { case .online: self.backgroundColor = ThemeService.shared().theme.tintColor self.borderLayer.borderColor = self.borderColor.cgColor + self.isHidden = false case .offline, .unavailable: self.backgroundColor = ThemeService.shared().theme.tabBarUnselectedItemTintColor self.borderLayer.borderColor = self.borderColor.cgColor + self.isHidden = false default: self.backgroundColor = UIColor.clear self.borderLayer.borderColor = UIColor.clear.cgColor + self.isHidden = true } } } - -// MARK: - Private Methods -private extension PresenceIndicatorView { - func setup() { - self.layer.addSublayer(borderLayer) - } -} // MARK: - CGRect Helper private extension CGRect { diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index d9affeb17..ffb429c01 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -27,15 +27,6 @@ #import "MXRoomSummary+Riot.h" -@interface RecentTableViewCell() -{ - /** - The observer of the presence for direct user. - */ - id mxDirectUserPresenceObserver; -} -@end - @implementation RecentTableViewCell #pragma mark - Class methods @@ -139,23 +130,13 @@ displayName:roomCellData.roomDisplayname mediaManager:roomCellData.mxSession.mediaManager]; - if (!mxDirectUserPresenceObserver && roomCellData.directUserId) + if (roomCellData.directUserId) { - // Observe contact presence change - MXWeakify(self); - mxDirectUserPresenceObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKContactManagerMatrixUserPresenceChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - MXStrongifyAndReturnIfNil(self); - - NSString* directUserId = self->roomCellData.directUserId; - - if (directUserId && [directUserId isEqualToString:notif.object]) - { - MXPresence presence = [MXTools presence:[notif.userInfo objectForKey:kMXKContactManagerMatrixPresenceKey]]; - [self refreshContactPresence:presence]; - } - }]; - - [self refreshContactPresence:roomCellData.presence]; + [self.presenceIndicatorView configureWithUserId:roomCellData.directUserId presence:roomCellData.presence]; + } + else + { + [self.presenceIndicatorView stopListeningPresenceUpdates]; } } else @@ -164,31 +145,11 @@ } } -- (void)refreshContactPresence:(MXPresence)presence -{ - self.presenceIndicatorView.presence = presence; - self.presenceIndicatorView.hidden = presence == MXPresenceUnknown; -} - - (void)prepareForReuse { [super prepareForReuse]; - [self removePresenceObserver]; -} - -- (void)dealloc -{ - [self removePresenceObserver]; -} - -- (void)removePresenceObserver -{ - if (mxDirectUserPresenceObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:mxDirectUserPresenceObserver]; - mxDirectUserPresenceObserver = nil; - } + [self.presenceIndicatorView stopListeningPresenceUpdates]; } + (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth diff --git a/Riot/Modules/Home/Views/RoomCollectionViewCell.m b/Riot/Modules/Home/Views/RoomCollectionViewCell.m index 10996778b..df7526d0b 100644 --- a/Riot/Modules/Home/Views/RoomCollectionViewCell.m +++ b/Riot/Modules/Home/Views/RoomCollectionViewCell.m @@ -26,15 +26,6 @@ #import "MXTools.h" -@interface RoomCollectionViewCell() -{ - /** - The observer of the presence for direct user. - */ - id mxDirectUserPresenceObserver; -} -@end - @implementation RoomCollectionViewCell #pragma mark - Class methods @@ -156,33 +147,17 @@ displayName:roomCellData.roomDisplayname mediaManager:roomCellData.mxSession.mediaManager]; - if (!mxDirectUserPresenceObserver && roomCellData.directUserId) + if (roomCellData.directUserId) { - // Observe contact presence change - MXWeakify(self); - mxDirectUserPresenceObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKContactManagerMatrixUserPresenceChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - MXStrongifyAndReturnIfNil(self); - - NSString* directUserId = self->roomCellData.directUserId; - - if (directUserId && [directUserId isEqualToString:notif.object]) - { - MXPresence presence = [MXTools presence:[notif.userInfo objectForKey:kMXKContactManagerMatrixPresenceKey]]; - [self refreshContactPresence:presence]; - } - }]; - - [self refreshContactPresence:roomCellData.presence]; + [self.presenceIndicatorView configureWithUserId:roomCellData.directUserId presence:roomCellData.presence]; + } + else + { + [self.presenceIndicatorView stopListeningPresenceUpdates]; } } } -- (void)refreshContactPresence:(MXPresence)presence -{ - self.presenceIndicatorView.presence = presence; - self.presenceIndicatorView.hidden = presence == MXPresenceUnknown; -} - - (MXKCellData*)renderedCellData { return roomCellData; @@ -203,7 +178,7 @@ { [super prepareForReuse]; - [self removePresenceObserver]; + [self.presenceIndicatorView stopListeningPresenceUpdates]; // Remove all gesture recognizers while (self.gestureRecognizers.count) @@ -218,24 +193,10 @@ roomCellData = nil; } -- (void)dealloc -{ - [self removePresenceObserver]; -} - - (NSString*)roomId { return roomCellData.roomIdentifier; } -- (void)removePresenceObserver -{ - if (mxDirectUserPresenceObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:mxDirectUserPresenceObserver]; - mxDirectUserPresenceObserver = nil; - } -} - @end diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift index 14f50131b..2cc802a7e 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift @@ -45,6 +45,7 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { encryptionImage: encryptionImage, isEncrypted: room.summary.isEncrypted, isDirect: room.isDirect, + directUserId: room.directUserId, directUserPresence: directUserPresence) return RoomInfoListViewData(numberOfMembers: Int(room.summary.membersCount.joined), @@ -58,12 +59,10 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { self.room = room super.init() startObservingSummaryChanges() - startObservingPresenceChanges() } deinit { stopObservingSummaryChanges() - stopObservingPresenceChanges() } // MARK: - Public @@ -91,27 +90,11 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { NotificationCenter.default.removeObserver(self, name: .mxRoomSummaryDidChange, object: nil) } - private func startObservingPresenceChanges() { - NotificationCenter.default.addObserver(self, selector: #selector(presenceUpdated(_:)), name: .mxkContactManagerMatrixUserPresenceChange, object: nil) - } - - private func stopObservingPresenceChanges() { - NotificationCenter.default.removeObserver(self, name: .mxkContactManagerMatrixUserPresenceChange, object: nil) - } - @objc private func roomSummaryUpdated(_ notification: Notification) { // force update view self.update(viewState: .loaded(viewData: viewData)) } - @objc private func presenceUpdated(_ notification: NSNotification) { - guard let updatedUserId = notification.object as? String, updatedUserId == room.directUserId else { - return - } - - self.update(viewState: .loaded(viewData: viewData)) - } - private func loadData() { self.update(viewState: .loaded(viewData: viewData)) } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift index be87ad01e..5d32a8918 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift @@ -30,7 +30,11 @@ class RoomInfoBasicView: UIView { @IBOutlet private weak var avatarContainerView: UIView! @IBOutlet private weak var avatarImageView: MXKImageView! @IBOutlet private weak var badgeImageView: UIImageView! - @IBOutlet private weak var presenceIndicatorView: PresenceIndicatorView! + @IBOutlet private weak var presenceIndicatorView: PresenceIndicatorView! { + didSet { + presenceIndicatorView.delegate = self + } + } @IBOutlet private weak var roomNameStackView: UIStackView! @IBOutlet private weak var roomNameLabel: UILabel! @IBOutlet private weak var roomAddressLabel: UILabel! @@ -98,12 +102,16 @@ class RoomInfoBasicView: UIView { VectorL10n.roomParticipantsSecurityInformationRoomEncryptedForDm : VectorL10n.roomParticipantsSecurityInformationRoomEncrypted securityContainerView.isHidden = !viewData.isEncrypted - presenceIndicatorView.setPresence(viewData.directUserPresence) - updateBadgeImageViewPosition(with: viewData.encryptionImage, presence: viewData.directUserPresence) + if let directUserId = viewData.directUserId { + presenceIndicatorView.configure(userId: directUserId, presence: viewData.directUserPresence) + } else { + presenceIndicatorView.stopListeningPresenceUpdates() + } + updateBadgeImageViewPosition(isPresenceDisplayed: viewData.directUserPresence != .unknown) } - private func updateBadgeImageViewPosition(with encryptionImage: UIImage?, presence: MXPresence) { - guard encryptionImage != nil else { + private func updateBadgeImageViewPosition(isPresenceDisplayed: Bool) { + guard badgeImageView.image != nil else { badgeImageView.isHidden = true return } @@ -111,7 +119,6 @@ class RoomInfoBasicView: UIView { badgeImageView.isHidden = false // Update badge position if it doesn't match expectation. // If presence is displayed, badge should be in the name stack. - let isPresenceDisplayed = presence != .unknown let isBadgeInRoomNameStackView = roomNameStackView.arrangedSubviews.contains(badgeImageView) switch (isPresenceDisplayed, isBadgeInRoomNameStackView) { case (true, false): @@ -167,3 +174,9 @@ extension RoomInfoBasicView: Themable { } } + +extension RoomInfoBasicView: PresenceIndicatorViewDelegate { + func presenceIndicatorViewDidUpdateVisibility(_ presenceIndicatorView: PresenceIndicatorView, isHidden: Bool) { + updateBadgeImageViewPosition(isPresenceDisplayed: !isHidden) + } +} diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicViewData.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicViewData.swift index 905e0cf00..210a5d5a6 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicViewData.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicViewData.swift @@ -27,5 +27,6 @@ struct RoomInfoBasicViewData { let encryptionImage: UIImage? let isEncrypted: Bool let isDirect: Bool + let directUserId: String? let directUserPresence: MXPresence } diff --git a/Riot/Modules/Room/Views/Title/RoomTitleView.h b/Riot/Modules/Room/Views/Title/RoomTitleView.h index eb0be2ef3..b95bfc016 100644 --- a/Riot/Modules/Room/Views/Title/RoomTitleView.h +++ b/Riot/Modules/Room/Views/Title/RoomTitleView.h @@ -21,6 +21,7 @@ // We add here a protocol to handle tap gesture in title view. @class RoomTitleView; @class PresenceIndicatorView; +@protocol PresenceIndicatorViewDelegate; @protocol RoomTitleViewTapGestureDelegate /** @@ -33,7 +34,7 @@ @end -@interface RoomTitleView : MXKRoomTitleView +@interface RoomTitleView : MXKRoomTitleView @property (weak, nonatomic) IBOutlet UIView *titleMask; @property (weak, nonatomic) IBOutlet UIImageView *badgeImageView; diff --git a/Riot/Modules/Room/Views/Title/RoomTitleView.m b/Riot/Modules/Room/Views/Title/RoomTitleView.m index 6572d4f08..b07f928d9 100644 --- a/Riot/Modules/Room/Views/Title/RoomTitleView.m +++ b/Riot/Modules/Room/Views/Title/RoomTitleView.m @@ -20,15 +20,6 @@ #import "ThemeService.h" #import "GeneratedInterface-Swift.h" -@interface RoomTitleView() -{ - /** - The observer of the presence for direct user. - */ - id mxDirectUserPresenceObserver; -} -@end - @implementation RoomTitleView + (UINib *)nib @@ -118,22 +109,16 @@ } else if (self.mxRoom) { - if (!mxDirectUserPresenceObserver && self.mxRoom.isDirect) + if (self.mxRoom.directUserId) { - // Observe contact presence change - MXWeakify(self); - mxDirectUserPresenceObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKContactManagerMatrixUserPresenceChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - MXStrongifyAndReturnIfNil(self); - - NSString* directUserId = self.mxRoom.directUserId; - - if (directUserId && [directUserId isEqualToString:notif.object]) - { - [self refreshContactPresence]; - } - }]; - - [self refreshContactPresence]; + MXUser *contact = [self.mxRoom.mxSession userWithUserId:self.mxRoom.directUserId]; + self.presenceIndicatorView.borderColor = ThemeService.shared.theme.headerBackgroundColor; + self.presenceIndicatorView.delegate = self; + [self.presenceIndicatorView configureWithUserId:self.mxRoom.directUserId presence:contact.presence]; + } + else + { + [self.presenceIndicatorView stopListeningPresenceUpdates]; } self.displayNameTextField.text = self.mxRoom.summary.displayname; @@ -149,38 +134,10 @@ } } -- (void)refreshContactPresence -{ - MXUser *contact = [self.mxRoom.mxSession userWithUserId:self.mxRoom.directUserId]; - BOOL presenceHidden = contact.presence == MXPresenceUnknown; - self.presenceIndicatorView.hidden = presenceHidden; - self.presenceIndicatorView.borderColor = ThemeService.shared.theme.headerBackgroundColor; - self.presenceIndicatorView.presence = contact.presence; - if (presenceHidden) - { - [self.badgeImageViewLeadingToPictureViewConstraint setPriority:UILayoutPriorityDefaultLow]; - [self.badgeImageViewCenterYToDisplayNameConstraint setPriority:UILayoutPriorityDefaultLow]; - [self.badgeImageViewToPictureViewBottomConstraint setPriority:UILayoutPriorityRequired]; - [self.badgeImageViewToPictureViewTrailingConstraint setPriority:UILayoutPriorityRequired]; - } - else - { - [self.badgeImageViewToPictureViewBottomConstraint setPriority:UILayoutPriorityDefaultLow]; - [self.badgeImageViewToPictureViewTrailingConstraint setPriority:UILayoutPriorityDefaultLow]; - [self.badgeImageViewLeadingToPictureViewConstraint setPriority:UILayoutPriorityRequired]; - [self.badgeImageViewCenterYToDisplayNameConstraint setPriority:UILayoutPriorityRequired]; - } -} - - (void)destroy { self.tapGestureDelegate = nil; - if (mxDirectUserPresenceObserver) { - [[NSNotificationCenter defaultCenter] removeObserver:mxDirectUserPresenceObserver]; - mxDirectUserPresenceObserver = nil; - } - [super destroy]; } @@ -246,4 +203,24 @@ return self.typingLabel.text; } +#pragma mark - PresenceIndicatorViewDelegate + +- (void)presenceIndicatorViewDidUpdateVisibility:(PresenceIndicatorView *)presenceIndicatorView isHidden:(BOOL)isHidden +{ + if (isHidden) + { + [self.badgeImageViewLeadingToPictureViewConstraint setPriority:UILayoutPriorityDefaultLow]; + [self.badgeImageViewCenterYToDisplayNameConstraint setPriority:UILayoutPriorityDefaultLow]; + [self.badgeImageViewToPictureViewBottomConstraint setPriority:UILayoutPriorityRequired]; + [self.badgeImageViewToPictureViewTrailingConstraint setPriority:UILayoutPriorityRequired]; + } + else + { + [self.badgeImageViewToPictureViewBottomConstraint setPriority:UILayoutPriorityDefaultLow]; + [self.badgeImageViewToPictureViewTrailingConstraint setPriority:UILayoutPriorityDefaultLow]; + [self.badgeImageViewLeadingToPictureViewConstraint setPriority:UILayoutPriorityRequired]; + [self.badgeImageViewCenterYToDisplayNameConstraint setPriority:UILayoutPriorityRequired]; + } +} + @end