Skip to content

Commit 64d9824

Browse files
authored
Merge pull request #6145 from vector-im/aringenbach/6144_presence_home_dm_refresh
Presence: fix live updates on Home & DM list
2 parents e0237d4 + cda862e commit 64d9824

File tree

12 files changed

+204
-96
lines changed

12 files changed

+204
-96
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// Copyright 2022 New Vector Ltd
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import Foundation
18+
19+
/// Dedicated listener object for a user Presence status.
20+
class PresenceIndicatorListener {
21+
// MARK: - Properties
22+
private let userId: String
23+
private var presence: MXPresence
24+
private let onUpdate: (MXPresence) -> Void
25+
private var presenceObserver: Any?
26+
27+
// MARK: - Setup
28+
/// Init.
29+
///
30+
/// - Parameters:
31+
/// - userId: the user id
32+
/// - presence: initial presence of the user
33+
/// - onUpdate: callback for Presence updates
34+
init(userId: String, presence: MXPresence, onUpdate: @escaping (MXPresence) -> Void) {
35+
self.userId = userId
36+
self.presence = presence
37+
self.onUpdate = onUpdate
38+
39+
NotificationCenter.default.addObserver(forName: .mxkContactManagerMatrixUserPresenceChange,
40+
object: nil,
41+
queue: .main) { [weak self] notif in
42+
guard let self = self,
43+
self.userId == notif.object as? String,
44+
let presenceString = notif.userInfo?[kMXKContactManagerMatrixPresenceKey] as? String else {
45+
return
46+
}
47+
48+
let newPresence = MXTools.presence(presenceString)
49+
50+
guard self.presence != newPresence else { return }
51+
52+
self.presence = newPresence
53+
self.onUpdate(newPresence)
54+
}
55+
}
56+
57+
deinit {
58+
if let presenceObserver = presenceObserver {
59+
NotificationCenter.default.removeObserver(presenceObserver)
60+
self.presenceObserver = nil
61+
}
62+
}
63+
}

Riot/Modules/Common/PresenceIndicator/PresenceIndicatorView.swift

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
import UIKit
1818

19+
/// Delegate for `PresenceIndicatorView`.
20+
@objc protocol PresenceIndicatorViewDelegate: AnyObject {
21+
func presenceIndicatorViewDidUpdateVisibility(_ presenceIndicatorView: PresenceIndicatorView, isHidden: Bool)
22+
}
23+
1924
/// `PresenceIndicatorView` is used to display a presence indicator over an avatar.
2025
@objcMembers
2126
@IBDesignable
@@ -24,9 +29,24 @@ final class PresenceIndicatorView: UIView {
2429
@IBInspectable var borderWidth: CGFloat = 0.0
2530
var borderColor: UIColor = ThemeService.shared().theme.backgroundColor
2631

27-
// MARK: - Private Properties
32+
// MARK: - Properties
33+
34+
// MARK: Private
2835
private let borderLayer = CALayer()
29-
36+
private var listener: PresenceIndicatorListener?
37+
38+
// MARK: Internal
39+
weak var delegate: PresenceIndicatorViewDelegate?
40+
41+
// MARK: Override
42+
override var isHidden: Bool {
43+
didSet {
44+
if oldValue != isHidden, let delegate = delegate {
45+
delegate.presenceIndicatorViewDidUpdateVisibility(self, isHidden: isHidden)
46+
}
47+
}
48+
}
49+
3050
// MARK: - Private Constants
3151
private enum Constants {
3252
static let borderLayerOffset: CGFloat = 1.0
@@ -57,6 +77,34 @@ final class PresenceIndicatorView: UIView {
5777
}
5878

5979
// MARK: - Internal Methods
80+
/// Configures the view and starts listening Presence updates for given user.
81+
///
82+
/// - Parameters:
83+
/// - userId: the user id
84+
/// - presence: the initial Presence of the user
85+
func configure(userId: String, presence: MXPresence) {
86+
setPresence(presence)
87+
self.listener = PresenceIndicatorListener(userId: userId,
88+
presence: presence) { [weak self] presence in
89+
guard let self = self else { return }
90+
self.setPresence(presence)
91+
}
92+
}
93+
94+
/// Stop listening to Presence updates and hides the indicator.
95+
/// This should be called before reuse or if current room moves from direct to non-direct.
96+
func stopListeningPresenceUpdates() {
97+
self.listener = nil
98+
self.isHidden = true
99+
}
100+
}
101+
102+
// MARK: - Private Methods
103+
private extension PresenceIndicatorView {
104+
func setup() {
105+
self.layer.addSublayer(borderLayer)
106+
}
107+
60108
/// Updates presence indicator with given `MXPresence`.
61109
///
62110
/// - Parameters:
@@ -66,22 +114,18 @@ final class PresenceIndicatorView: UIView {
66114
case .online:
67115
self.backgroundColor = ThemeService.shared().theme.tintColor
68116
self.borderLayer.borderColor = self.borderColor.cgColor
117+
self.isHidden = false
69118
case .offline, .unavailable:
70119
self.backgroundColor = ThemeService.shared().theme.tabBarUnselectedItemTintColor
71120
self.borderLayer.borderColor = self.borderColor.cgColor
121+
self.isHidden = false
72122
default:
73123
self.backgroundColor = UIColor.clear
74124
self.borderLayer.borderColor = UIColor.clear.cgColor
125+
self.isHidden = true
75126
}
76127
}
77128
}
78-
79-
// MARK: - Private Methods
80-
private extension PresenceIndicatorView {
81-
func setup() {
82-
self.layer.addSublayer(borderLayer)
83-
}
84-
}
85129

86130
// MARK: - CGRect Helper
87131
private extension CGRect {

Riot/Modules/Common/Recents/Views/RecentTableViewCell.m

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ - (void)customizeTableViewCellRendering
4848
self.lastEventDescription.textColor = ThemeService.shared.theme.textSecondaryColor;
4949
self.lastEventDate.textColor = ThemeService.shared.theme.textSecondaryColor;
5050
self.missedNotifAndUnreadBadgeLabel.textColor = ThemeService.shared.theme.baseTextPrimaryColor;
51+
self.presenceIndicatorView.borderColor = ThemeService.shared.theme.backgroundColor;
5152

5253
self.roomAvatar.defaultBackgroundColor = [UIColor clearColor];
5354
}
@@ -128,18 +129,29 @@ - (void)render:(MXKCellData *)cellData
128129
roomId:roomCellData.roomIdentifier
129130
displayName:roomCellData.roomDisplayname
130131
mediaManager:roomCellData.mxSession.mediaManager];
131-
132-
// Presence indicator
133-
self.presenceIndicatorView.borderColor = ThemeService.shared.theme.backgroundColor;
134-
self.presenceIndicatorView.presence = roomCellData.presence;
135-
self.presenceIndicatorView.hidden = roomCellData.presence == MXPresenceUnknown;
132+
133+
if (roomCellData.directUserId)
134+
{
135+
[self.presenceIndicatorView configureWithUserId:roomCellData.directUserId presence:roomCellData.presence];
136+
}
137+
else
138+
{
139+
[self.presenceIndicatorView stopListeningPresenceUpdates];
140+
}
136141
}
137142
else
138143
{
139144
self.lastEventDescription.text = @"";
140145
}
141146
}
142147

148+
- (void)prepareForReuse
149+
{
150+
[super prepareForReuse];
151+
152+
[self.presenceIndicatorView stopListeningPresenceUpdates];
153+
}
154+
143155
+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth
144156
{
145157
// The height is fixed

Riot/Modules/Home/Views/RoomCollectionViewCell.m

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,15 @@ - (void)render:(MXKCellData *)cellData
146146
roomId:roomCellData.roomIdentifier
147147
displayName:roomCellData.roomDisplayname
148148
mediaManager:roomCellData.mxSession.mediaManager];
149-
150-
// Presence indicator
151-
self.presenceIndicatorView.presence = roomCellData.presence;
152-
self.presenceIndicatorView.hidden = roomCellData.presence == MXPresenceUnknown;
149+
150+
if (roomCellData.directUserId)
151+
{
152+
[self.presenceIndicatorView configureWithUserId:roomCellData.directUserId presence:roomCellData.presence];
153+
}
154+
else
155+
{
156+
[self.presenceIndicatorView stopListeningPresenceUpdates];
157+
}
153158
}
154159
}
155160

@@ -172,7 +177,9 @@ + (CGSize)defaultCellSize
172177
- (void)prepareForReuse
173178
{
174179
[super prepareForReuse];
175-
180+
181+
[self.presenceIndicatorView stopListeningPresenceUpdates];
182+
176183
// Remove all gesture recognizers
177184
while (self.gestureRecognizers.count)
178185
{

Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ - (NSString *)avatarUrl
9393
return roomSummary.avatar;
9494
}
9595

96+
- (NSString *)directUserId
97+
{
98+
return self.roomSummary.directUserId;
99+
}
100+
96101
- (MXPresence)presence
97102
{
98103
if (self.roomSummary.isDirect)

Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
@property (nonatomic, readonly) NSString *roomIdentifier;
4545
@property (nonatomic, readonly) NSString *roomDisplayname;
4646
@property (nonatomic, readonly) NSString *avatarUrl;
47+
@property (nonatomic, readonly) NSString *directUserId;
4748
@property (nonatomic, readonly) MXPresence presence;
4849
@property (nonatomic, readonly) NSString *lastEventTextMessage;
4950
@property (nonatomic, readonly) NSString *lastEventDate;

Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType {
4545
encryptionImage: encryptionImage,
4646
isEncrypted: room.summary.isEncrypted,
4747
isDirect: room.isDirect,
48+
directUserId: room.directUserId,
4849
directUserPresence: directUserPresence)
4950

5051
return RoomInfoListViewData(numberOfMembers: Int(room.summary.membersCount.joined),
@@ -58,12 +59,10 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType {
5859
self.room = room
5960
super.init()
6061
startObservingSummaryChanges()
61-
startObservingPresenceChanges()
6262
}
6363

6464
deinit {
6565
stopObservingSummaryChanges()
66-
stopObservingPresenceChanges()
6766
}
6867

6968
// MARK: - Public
@@ -91,27 +90,11 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType {
9190
NotificationCenter.default.removeObserver(self, name: .mxRoomSummaryDidChange, object: nil)
9291
}
9392

94-
private func startObservingPresenceChanges() {
95-
NotificationCenter.default.addObserver(self, selector: #selector(presenceUpdated(_:)), name: .mxkContactManagerMatrixUserPresenceChange, object: nil)
96-
}
97-
98-
private func stopObservingPresenceChanges() {
99-
NotificationCenter.default.removeObserver(self, name: .mxkContactManagerMatrixUserPresenceChange, object: nil)
100-
}
101-
10293
@objc private func roomSummaryUpdated(_ notification: Notification) {
10394
// force update view
10495
self.update(viewState: .loaded(viewData: viewData))
10596
}
10697

107-
@objc private func presenceUpdated(_ notification: NSNotification) {
108-
guard let updatedUserId = notification.object as? String, updatedUserId == room.directUserId else {
109-
return
110-
}
111-
112-
self.update(viewState: .loaded(viewData: viewData))
113-
}
114-
11598
private func loadData() {
11699
self.update(viewState: .loaded(viewData: viewData))
117100
}

Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ class RoomInfoBasicView: UIView {
3030
@IBOutlet private weak var avatarContainerView: UIView!
3131
@IBOutlet private weak var avatarImageView: MXKImageView!
3232
@IBOutlet private weak var badgeImageView: UIImageView!
33-
@IBOutlet private weak var presenceIndicatorView: PresenceIndicatorView!
33+
@IBOutlet private weak var presenceIndicatorView: PresenceIndicatorView! {
34+
didSet {
35+
presenceIndicatorView.delegate = self
36+
}
37+
}
3438
@IBOutlet private weak var roomNameStackView: UIStackView!
3539
@IBOutlet private weak var roomNameLabel: UILabel!
3640
@IBOutlet private weak var roomAddressLabel: UILabel!
@@ -98,20 +102,23 @@ class RoomInfoBasicView: UIView {
98102
VectorL10n.roomParticipantsSecurityInformationRoomEncryptedForDm :
99103
VectorL10n.roomParticipantsSecurityInformationRoomEncrypted
100104
securityContainerView.isHidden = !viewData.isEncrypted
101-
presenceIndicatorView.setPresence(viewData.directUserPresence)
102-
updateBadgeImageViewPosition(with: viewData.encryptionImage, presence: viewData.directUserPresence)
105+
if let directUserId = viewData.directUserId {
106+
presenceIndicatorView.configure(userId: directUserId, presence: viewData.directUserPresence)
107+
} else {
108+
presenceIndicatorView.stopListeningPresenceUpdates()
109+
}
110+
updateBadgeImageViewPosition(isPresenceDisplayed: viewData.directUserPresence != .unknown)
103111
}
104112

105-
private func updateBadgeImageViewPosition(with encryptionImage: UIImage?, presence: MXPresence) {
106-
guard encryptionImage != nil else {
113+
private func updateBadgeImageViewPosition(isPresenceDisplayed: Bool) {
114+
guard badgeImageView.image != nil else {
107115
badgeImageView.isHidden = true
108116
return
109117
}
110118

111119
badgeImageView.isHidden = false
112120
// Update badge position if it doesn't match expectation.
113121
// If presence is displayed, badge should be in the name stack.
114-
let isPresenceDisplayed = presence != .unknown
115122
let isBadgeInRoomNameStackView = roomNameStackView.arrangedSubviews.contains(badgeImageView)
116123
switch (isPresenceDisplayed, isBadgeInRoomNameStackView) {
117124
case (true, false):
@@ -167,3 +174,9 @@ extension RoomInfoBasicView: Themable {
167174
}
168175

169176
}
177+
178+
extension RoomInfoBasicView: PresenceIndicatorViewDelegate {
179+
func presenceIndicatorViewDidUpdateVisibility(_ presenceIndicatorView: PresenceIndicatorView, isHidden: Bool) {
180+
updateBadgeImageViewPosition(isPresenceDisplayed: !isHidden)
181+
}
182+
}

Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicViewData.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ struct RoomInfoBasicViewData {
2727
let encryptionImage: UIImage?
2828
let isEncrypted: Bool
2929
let isDirect: Bool
30+
let directUserId: String?
3031
let directUserPresence: MXPresence
3132
}

Riot/Modules/Room/Views/Title/RoomTitleView.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
// We add here a protocol to handle tap gesture in title view.
2222
@class RoomTitleView;
2323
@class PresenceIndicatorView;
24+
@protocol PresenceIndicatorViewDelegate;
2425
@protocol RoomTitleViewTapGestureDelegate <NSObject>
2526

2627
/**
@@ -33,7 +34,7 @@
3334

3435
@end
3536

36-
@interface RoomTitleView : MXKRoomTitleView <UIGestureRecognizerDelegate>
37+
@interface RoomTitleView : MXKRoomTitleView <UIGestureRecognizerDelegate, PresenceIndicatorViewDelegate>
3738

3839
@property (weak, nonatomic) IBOutlet UIView *titleMask;
3940
@property (weak, nonatomic) IBOutlet UIImageView *badgeImageView;

0 commit comments

Comments
 (0)