Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NextcloudTalk/Contacts/ContactsTableViewCell.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ extern CGFloat const kContactsTableCellTitleFontSize;
@property(nonatomic, weak) IBOutlet AvatarView *avatarView;
@property(nonatomic, weak) IBOutlet UILabel *labelTitle;
@property (weak, nonatomic) IBOutlet UILabel *userStatusMessageLabel;
@property (nonatomic, strong) UIImageView *roleIconView;

- (void)setUserStatusMessage:(NSString * _Nullable)userStatusMessage withIcon:(NSString * _Nullable)userStatusIcon;
- (void)setRoleIcon:(NSString * _Nullable)systemImageName withAccessibilityLabel:(NSString * _Nullable)accessibilityLabel;

@end
57 changes: 57 additions & 0 deletions NextcloudTalk/Contacts/ContactsTableViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,51 @@

CGFloat const kContactsTableCellHeight = 72.0f;
CGFloat const kContactsTableCellTitleFontSize = 17.0f;
static CGFloat const kContactsTableCellRoleIconSize = 15.0f;
static CGFloat const kContactsTableCellRoleIconSpacing = 4.0f;

@interface ContactsTableViewCell ()

@property (weak, nonatomic) IBOutlet UIStackView *titleStackView;
@property (nonatomic, strong) NSLayoutConstraint *roleIconLeadingConstraint;
@property (nonatomic, strong) NSLayoutConstraint *roleIconWidthConstraint;

@end

@implementation ContactsTableViewCell

- (void)awakeFromNib
{
[super awakeFromNib];

[self setupRoleIconView];
}

- (void)setupRoleIconView
{
self.roleIconView = [[UIImageView alloc] init];
self.roleIconView.translatesAutoresizingMaskIntoConstraints = NO;
self.roleIconView.contentMode = UIViewContentModeScaleAspectFit;
self.roleIconView.tintColor = [UIColor secondaryLabelColor];
[self.roleIconView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[self.contentView addSubview:self.roleIconView];

// The labels are leading-aligned in their stack view and hug their text, so the
// icon can follow the name directly instead of sitting at the edge of the cell
self.roleIconLeadingConstraint = [self.roleIconView.leadingAnchor constraintEqualToAnchor:self.labelTitle.trailingAnchor];
self.roleIconWidthConstraint = [self.roleIconView.widthAnchor constraintEqualToConstant:0];

[NSLayoutConstraint activateConstraints:@[
self.roleIconLeadingConstraint,
self.roleIconWidthConstraint,
[self.roleIconView.centerYAnchor constraintEqualToAnchor:self.labelTitle.centerYAnchor],
// Truncate the name rather than the icon
[self.roleIconView.trailingAnchor constraintLessThanOrEqualToAnchor:self.titleStackView.trailingAnchor],
[self.labelTitle.trailingAnchor constraintLessThanOrEqualToAnchor:self.titleStackView.trailingAnchor],
[self.userStatusMessageLabel.trailingAnchor constraintLessThanOrEqualToAnchor:self.titleStackView.trailingAnchor]
]];
}

- (void)prepareForReuse
{
[super prepareForReuse];
Expand All @@ -30,6 +72,21 @@ - (void)prepareForReuse
self.labelTitle.textColor = [UIColor labelColor];

self.labelTitle.font = [UIFont systemFontOfSize:kContactsTableCellTitleFontSize weight:UIFontWeightRegular];

[self setRoleIcon:nil withAccessibilityLabel:nil];
}

- (void)setRoleIcon:(NSString *)systemImageName withAccessibilityLabel:(NSString *)accessibilityLabel
{
UIImageSymbolConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:kContactsTableCellRoleIconSize];
UIImage *icon = systemImageName ? [UIImage systemImageNamed:systemImageName withConfiguration:configuration] : nil;

self.roleIconView.image = icon;
self.roleIconView.accessibilityLabel = accessibilityLabel;

// Without an icon the view collapses so it takes no space after the name
self.roleIconLeadingConstraint.constant = icon ? kContactsTableCellRoleIconSpacing : 0;
self.roleIconWidthConstraint.constant = icon.size.width;
}

- (void)setUserStatusMessage:(NSString *)userStatusMessage withIcon:(NSString *)userStatusIcon
Expand Down
23 changes: 9 additions & 14 deletions NextcloudTalk/Contacts/ContactsTableViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ struct ContactsTableViewCellWrapper: UIViewRepresentable {
}

func updateUIView(_ cell: ContactsTableViewCell, context: Context) {
if participant.canModerate, (room.type == .oneToOne || room.type == .formerOneToOne || room.type == .noteToSelf) {
cell.labelTitle.text = participant.displayName
} else {
cell.labelTitle.text = participant.detailedName
}
cell.labelTitle.text = participant.detailedName

let roleIcon = participant.roleIcon(in: room)
cell.setRoleIcon(roleIcon?.systemImageName, withAccessibilityLabel: roleIcon?.localizedName)

cell.avatarView.setStatus(for: participant, inRoom: room)
cell.setUserStatusMessage(participant.statusMessage, withIcon: participant.statusIcon)
Expand All @@ -40,15 +39,11 @@ struct ContactsTableViewCellWrapper: UIViewRepresentable {
cell.setUserStatusMessage(invitedActorId, withIcon: nil)
}

if participant.isOffline {
cell.avatarView.alpha = 0.5
cell.labelTitle.alpha = 0.5
cell.userStatusMessageLabel.alpha = 0.5
} else {
cell.avatarView.alpha = 1
cell.labelTitle.alpha = 1
cell.userStatusMessageLabel.alpha = 1
}
let contentAlpha: CGFloat = participant.isOffline ? 0.5 : 1
cell.avatarView.alpha = contentAlpha
cell.labelTitle.alpha = contentAlpha
cell.userStatusMessageLabel.alpha = contentAlpha
cell.roleIconView.alpha = contentAlpha

if let callIconImageName = participant.callIconImageName, !callIconImageName.isEmpty {
cell.accessoryView = UIImageView(image: .init(systemName: callIconImageName))
Expand Down
3 changes: 2 additions & 1 deletion NextcloudTalk/Contacts/ContactsTableViewCell.xib
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<constraint firstAttribute="width" secondItem="Uyr-mW-eAX" secondAttribute="height" multiplier="1:1" id="j4M-gw-fjI"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" translatesAutoresizingMaskIntoConstraints="NO" id="ifa-Ff-Eac">
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="leading" distribution="fillProportionally" translatesAutoresizingMaskIntoConstraints="NO" id="ifa-Ff-Eac">
<rect key="frame" x="72" y="12" width="233" height="48"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="17" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nOA-bG-zXZ">
Expand Down Expand Up @@ -61,6 +61,7 @@
<connections>
<outlet property="avatarView" destination="Uyr-mW-eAX" id="Jig-yS-5UL"/>
<outlet property="labelTitle" destination="nOA-bG-zXZ" id="0dX-jq-5N3"/>
<outlet property="titleStackView" destination="ifa-Ff-Eac" id="tSv-1a-Bc2"/>
<outlet property="userStatusMessageLabel" destination="27H-pY-tFT" id="kAG-y7-hW7"/>
</connections>
<point key="canvasLocation" x="33.600000000000001" y="45.877061469265371"/>
Expand Down
39 changes: 34 additions & 5 deletions NextcloudTalk/Rooms/NCRoomParticipant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ enum AttendeeType: String {
case bots = "bots"
}

public enum ParticipantRole {
case owner
case moderator

public var systemImageName: String {
switch self {
case .owner: return "crown"
case .moderator: return "shield"
}
}

public var localizedName: String {
switch self {
case .owner: return NSLocalizedString("Owner", comment: "Conversation owner, a participant with high-level permissions")
case .moderator: return NSLocalizedString("Moderator", comment: "Conversation moderator, a participant with elevated permissions")
}
}
}

@objcMembers
public class NCRoomParticipant: NSObject {

Expand Down Expand Up @@ -66,6 +85,10 @@ public class NCRoomParticipant: NSObject {
super.init()
}

public var isOwner: Bool {
return participantType == .owner
}

public var canModerate: Bool {
return participantType == .owner || participantType == .moderator || participantType == .guestModerator
}
Expand Down Expand Up @@ -133,11 +156,6 @@ public class NCRoomParticipant: NSObject {
}
}

// Moderator label
if canModerate {
detailedNameString = String(format: NSLocalizedString("%@ (moderator)", comment: "'Alice (moderator)' - a participant who can moderate the conversation"), detailedNameString)
}

// Bridge bot label
if isBridgeBotUser {
detailedNameString = String(format: NSLocalizedString("%@ (bot)", comment: "'Alice (bot)' - a participant that is a bot, not a person"), detailedNameString)
Expand All @@ -151,6 +169,17 @@ public class NCRoomParticipant: NSObject {
return detailedNameString
}

// Ranks are only marked in conversations that actually have them: both
// participants of a one-to-one are owners by design.
public func roleIcon(in room: NCRoom) -> ParticipantRole? {
guard canModerate,
room.type != .oneToOne, room.type != .formerOneToOne,
room.type != .changelog, room.type != .noteToSelf
else { return nil }

return isOwner ? .owner : .moderator
}

public var callIconImageName: String? {
guard !inCall.isEmpty else { return nil }

Expand Down
9 changes: 5 additions & 4 deletions NextcloudTalk/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@
/* 'Alice (guest)' - a participant who is not logged in */
"%@ (guest)" = "%@ (guest)";

/* 'Alice (moderator)' - a participant who can moderate the conversation */
"%@ (moderator)" = "%@ (moderator)";

/* Alice and Bob will receive invitations */
"%@ and %@ will receive invitations" = "%1$@ and %2$@ will receive invitations";

Expand Down Expand Up @@ -1682,6 +1679,9 @@
/* No comment provided by engineer. */
"moderator" = "moderator";

/* Conversation moderator, a participant with elevated permissions */
"Moderator" = "Moderator";

/* No comment provided by engineer. */
"Modification date" = "Modification date";

Expand Down Expand Up @@ -1925,7 +1925,8 @@
/* No comment provided by engineer. */
"Outgoing silent call" = "Outgoing silent call";

/* Owner of a repository */
/* Conversation owner, a participant with high-level permissions
Owner of a repository */
"Owner" = "Owner";

/* No comment provided by engineer. */
Expand Down
33 changes: 33 additions & 0 deletions NextcloudTalkTests/Unit/UnitNCRoomParticipantTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,39 @@ final class UnitNCRoomParticipantTest: TestBaseRealm {
return participant
}

func createRoom(withType type: NCRoomType) -> NCRoom {
let room = NCRoom()
room.type = type
return room
}

func testRoleIcon() throws {
let groupRoom = createRoom(withType: .group)

let owner = createRoomParticipant(withDisplayName: "Owner")
owner.participantType = .owner

let moderator = createRoomParticipant(withDisplayName: "Moderator", isModerator: true)

let guestModerator = createRoomParticipant(withDisplayName: "Guest moderator", withActorType: .guest)
guestModerator.participantType = .guestModerator

let user = createRoomParticipant(withDisplayName: "User")
user.participantType = .user

XCTAssertEqual(owner.roleIcon(in: groupRoom), .owner)
XCTAssertEqual(moderator.roleIcon(in: groupRoom), .moderator)
XCTAssertEqual(guestModerator.roleIcon(in: groupRoom), .moderator)
XCTAssertNil(user.roleIcon(in: groupRoom))

// Conversations without ranks never mark a role
for type in [NCRoomType.oneToOne, .formerOneToOne, .changelog, .noteToSelf] {
let room = createRoom(withType: type)
XCTAssertNil(owner.roleIcon(in: room), "Unexpected role icon in room type \(type)")
XCTAssertNil(moderator.roleIcon(in: room), "Unexpected role icon in room type \(type)")
}
}

func testSorting() throws {
let team9 = createRoomParticipant(withDisplayName: "9. Team", withActorType: .teams)
let team8 = createRoomParticipant(withDisplayName: "8. Team", withActorType: .teams)
Expand Down
Loading