Skip to content

Commit

Permalink
Merge pull request #1793 from nextcloud/user-status-in-settings-button
Browse files Browse the repository at this point in the history
User status in settings button
  • Loading branch information
Ivansss authored Sep 18, 2024
2 parents e775bde + 7252a9a commit c139836
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 19 deletions.
4 changes: 4 additions & 0 deletions NextcloudTalk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@
2CB6ACEE2641954700D3D641 /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CB6ACE82641954700D3D641 /* MapViewController.xib */; };
2CB997C52A052449003C41AC /* EmojiAvatarPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */; };
2CB997C62A052449003C41AC /* EmojiAvatarPickerViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */; };
2CBD0D5A2C8770A40013C089 /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD0D592C8770A40013C089 /* UIImageExtension.swift */; };
2CBF82AE1FC888FC00636459 /* NCPushNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82AD1FC888FC00636459 /* NCPushNotification.m */; };
2CBF82B21FCC7DBA00636459 /* CCCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82B11FCC7DBA00636459 /* CCCertificate.m */; };
2CC0015324A1F0E900A20167 /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC0015224A1F0E900A20167 /* NotificationService.m */; };
Expand Down Expand Up @@ -1110,6 +1111,7 @@
2CB6ACE82641954700D3D641 /* MapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MapViewController.xib; sourceTree = "<group>"; };
2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiAvatarPickerViewController.swift; sourceTree = "<group>"; };
2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmojiAvatarPickerViewController.xib; sourceTree = "<group>"; };
2CBD0D592C8770A40013C089 /* UIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = "<group>"; };
2CBF82AC1FC888FC00636459 /* NCPushNotification.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCPushNotification.h; sourceTree = "<group>"; };
2CBF82AD1FC888FC00636459 /* NCPushNotification.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCPushNotification.m; sourceTree = "<group>"; };
2CBF82B01FCC7DBA00636459 /* CCCertificate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CCCertificate.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1612,6 +1614,7 @@
2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */,
1FAB2EED2AD1BC1B001214EB /* UIControlExtensions.swift */,
1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */,
2CBD0D592C8770A40013C089 /* UIImageExtension.swift */,
1F1B0F312BDC57E3003FD766 /* UIPageViewControllerExtension.swift */,
1F1B0F352BDD8B9C003FD766 /* NCActivityIndicator.swift */,
);
Expand Down Expand Up @@ -2940,6 +2943,7 @@
2CF8AD3F2A0010FB00A4D3E6 /* MessageTranslationViewController.swift in Sources */,
2C21446E2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift in Sources */,
2C4230F72B207AB00013E1FA /* ContextChatViewController.swift in Sources */,
2CBD0D5A2C8770A40013C089 /* UIImageExtension.swift in Sources */,
1F90DA0429E9A28E00E81E3D /* AvatarManager.swift in Sources */,
1F1DF8432C64006E00E5EA86 /* SignalingParticipant.swift in Sources */,
2CC1FF4428147F11009F7288 /* RoomSharedItemsTableViewController.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions NextcloudTalk/NCUserStatus.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ extern NSString * const kUserStatusOffline;
- (NSString *)readableUserStatusMessage;
- (NSString *)readableUserStatusOrMessage;
- (nullable UIImage *)getSFUserStatusIcon;
- (BOOL)hasVisibleStatusIcon;

@end

Expand Down
8 changes: 8 additions & 0 deletions NextcloudTalk/NCUserStatus.m
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,12 @@ - (UIImage *)getSFUserStatusIcon
return [UIImage systemImageNamed:@"person.fill.questionmark"];
}

- (BOOL)hasVisibleStatusIcon
{
return [_status isEqualToString:kUserStatusOnline] ||
[_status isEqualToString:kUserStatusAway] ||
[_status isEqualToString:kUserStatusDND] ||
[_status isEqualToString:kUserStatusInvisible];
}

@end
86 changes: 70 additions & 16 deletions NextcloudTalk/RoomsTableViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
kRoomsSectionRoomList
} RoomsSections;

@interface RoomsTableViewController () <CCCertificateDelegate, UISearchBarDelegate, UISearchControllerDelegate, UISearchResultsUpdating>
@interface RoomsTableViewController () <CCCertificateDelegate, UISearchBarDelegate, UISearchControllerDelegate, UISearchResultsUpdating, UserStatusViewDelegate>
{
RLMNotificationToken *_rlmNotificationToken;
NSMutableArray *_rooms;
Expand All @@ -56,6 +56,7 @@ @interface RoomsTableViewController () <CCCertificateDelegate, UISearchBarDelega
UIBarButtonItem *_newConversationButton;
UIBarButtonItem *_settingsButton;
UIButton *_profileButton;
NCUserStatus *_activeUserStatus;
NSTimer *_refreshRoomsTimer;
NSIndexPath *_nextRoomWithMentionIndexPath;
NSIndexPath *_lastRoomWithMentionIndexPath;
Expand Down Expand Up @@ -404,6 +405,8 @@ - (void)refreshRooms
[[NCRoomsManager sharedInstance] resendOfflineMessagesWithCompletionBlock:nil];
}

[self getUserStatusWithCompletionBlock:nil];

dispatch_async(dispatch_get_main_queue(), ^{
// Dispatch to main, otherwise the traitCollection is not updated yet and profile buttons shows wrong style
[self setUnreadMessageForInactiveAccountsIndicator];
Expand Down Expand Up @@ -433,11 +436,19 @@ - (void)refreshControlTarget

// When we manually forced a room update, we update the invitation list as well
[[NCRoomsManager sharedInstance] updatePendingFederationInvitations];
[self getUserStatusWithCompletionBlock:nil];

// Actuate `Peek` feedback (weak boom)
AudioServicesPlaySystemSound(1519);
}

#pragma mark - User Status SwiftUI View Delegate

- (void)userStatusViewDidDisappear
{
[self getUserStatusWithCompletionBlock:nil];
}

#pragma mark - Title menu

- (void)setNavigationLogoButton
Expand All @@ -459,15 +470,15 @@ - (UIMenu *)getActiveAccountMenuOptions
return;
}

[[NCAPIController sharedInstance] getUserStatusForAccount:activeAccount withCompletionBlock:^(NSDictionary *userStatusDict, NSError *error) {
[self getUserStatusWithCompletionBlock:^(NSDictionary *userStatusDict, NSError *error) {
if (error) {
completion(@[]);
return;
}

NCUserStatus *userStatus = [NCUserStatus userStatusWithDictionary:userStatusDict];
UIImage *userStatusImage = [userStatus getSFUserStatusIcon];
UIViewController *vc = [UserStatusSwiftUIViewFactory createWithUserStatus:userStatus];
UIViewController *vc = [UserStatusSwiftUIViewFactory createWithUserStatus:userStatus delegate:self];

UIAction *onlineOption = [UIAction actionWithTitle:[userStatus readableUserStatusOrMessage] image:userStatusImage identifier:nil handler:^(UIAction *action) {
[self presentViewController:vc animated:YES completion:nil];
Expand Down Expand Up @@ -798,6 +809,8 @@ - (void)adaptInterfaceForAppState:(AppState)appState
case kAppStateMissingServerCapabilities:
case kAppStateMissingSignalingConfiguration:
{
// Clear active user status when changing users
_activeUserStatus = nil;
[self setProfileButton];
}
break;
Expand All @@ -806,6 +819,7 @@ - (void)adaptInterfaceForAppState:(AppState)appState
[self setProfileButton];
BOOL isAppActive = [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive;
[[NCRoomsManager sharedInstance] updateRoomsUpdatingUserStatus:isAppActive onlyLastModified:NO];
[self getUserStatusWithCompletionBlock:nil];
[self startRefreshRoomsTimer];
[self setupNavigationBar];
}
Expand Down Expand Up @@ -934,31 +948,71 @@ - (void)calculateLastRoomWithMention
- (void)setProfileButton
{
_profileButton = [UIButton buttonWithType:UIButtonTypeCustom];
_profileButton.frame = CGRectMake(0, 0, 30, 30);
_profileButton.frame = CGRectMake(0, 0, 38, 38);
_profileButton.accessibilityLabel = NSLocalizedString(@"User profile and settings", nil);

_settingsButton = [[UIBarButtonItem alloc] initWithCustomView:_profileButton];
[self.navigationItem setLeftBarButtonItem:_settingsButton];

[self updateProfileButtonImage];
[self updateAccountPickerMenu];
[self setUnreadMessageForInactiveAccountsIndicator];
}

- (void)updateProfileButtonImage
{
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
UIImage *profileImage = [[NCAPIController sharedInstance] userProfileImageForAccount:activeAccount withStyle:self.traitCollection.userInterfaceStyle];
if (profileImage) {
UIGraphicsBeginImageContextWithOptions(_profileButton.bounds.size, NO, 3.0);
[[UIBezierPath bezierPathWithRoundedRect:_profileButton.bounds cornerRadius:_profileButton.bounds.size.height] addClip];
[profileImage drawInRect:_profileButton.bounds];
profileImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[_profileButton setImage:profileImage forState:UIControlStateNormal];
// Crop the profile image into a circle
profileImage = [profileImage cropToCircleWithSize:CGSizeMake(30, 30)];
// Increase the profile image size to leave space for the status
profileImage = [profileImage withCircularBackgroundWithBackgroundColor:[UIColor clearColor] diameter:38.0 padding:4.0];

// Online status icon
UIImage *statusImage = nil;
if ([_activeUserStatus hasVisibleStatusIcon]) {
statusImage = [[_activeUserStatus getSFUserStatusIcon] withCircularBackgroundWithBackgroundColor:self.navigationController.navigationBar.barTintColor
diameter:14.0 padding:2.0];
}

// Status message icon
if (_activeUserStatus.icon.length > 0) {
UILabel *iconLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 14, 14)];
iconLabel.text = _activeUserStatus.icon;
iconLabel.adjustsFontSizeToFitWidth = YES;
statusImage = [UIImage imageFrom:iconLabel];
}

// Set status image
if (statusImage) {
profileImage = [profileImage overlayWith:statusImage at:CGRectMake(24, 24, 14, 14)];
}

[_profileButton setImage:profileImage forState:UIControlStateNormal];
// Used to distinguish between a "completely loaded" button (with a profile image) and the default gear one
_profileButton.accessibilityIdentifier = @"LoadedProfileButton";
} else {
[_profileButton setImage:[UIImage systemImageNamed:@"gear"] forState:UIControlStateNormal];
_profileButton.contentMode = UIViewContentModeCenter;
}

_settingsButton = [[UIBarButtonItem alloc] initWithCustomView:_profileButton];
[self updateAccountPickerMenu];
[self setUnreadMessageForInactiveAccountsIndicator];

[self.navigationItem setLeftBarButtonItem:_settingsButton];
}

- (void)getUserStatusWithCompletionBlock:(GetUserStatusCompletionBlock)block
{
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
[[NCAPIController sharedInstance] getUserStatusForAccount:activeAccount withCompletionBlock:^(NSDictionary *userStatusDict, NSError *error) {
if (!error) {
self->_activeUserStatus = [NCUserStatus userStatusWithDictionary:userStatusDict];
[self updateProfileButtonImage];

if (block) {
block(userStatusDict, nil);
}
} else if (block) {
block(nil, error);
}
}];
}

- (void)setUnreadMessageForInactiveAccountsIndicator
Expand Down
106 changes: 106 additions & 0 deletions NextcloudTalk/UIImageExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//

import UIKit

extension UIImage {

// Function to overlay an image on top of the current image
@objc func overlay(with overlayImage: UIImage, at overlayRect: CGRect) -> UIImage? {
// Calculate the new size for the resulting image
let newWidth = max(self.size.width, overlayRect.origin.x + overlayRect.size.width)
let newHeight = max(self.size.height, overlayRect.origin.y + overlayRect.size.height)
let newSize = CGSize(width: newWidth, height: newHeight)

// Begin a new image context with the new size.
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)

// Draw the base image in its original position.
self.draw(in: CGRect(origin: CGPoint.zero, size: self.size))

// Draw the overlay image in the specified rectangle.
overlayImage.draw(in: overlayRect)

// Capture the new image from the context.
let newImage = UIGraphicsGetImageFromCurrentImageContext()

// End the image context to free up memory.
UIGraphicsEndImageContext()

return newImage
}

// Function to crop an image into a circle with the specified size.
@objc func cropToCircle(withSize size: CGSize) -> UIImage? {
// Begin a new image context with the target size
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)

// Create a circular path using a rounded rectangle
let rect = CGRect(origin: .zero, size: size)
let path = UIBezierPath(roundedRect: rect, cornerRadius: size.width / 2)
path.addClip()

// Draw the image in the context, scaled to fill the entire circular area
self.draw(in: rect)

// Capture the new image
let circleImage = UIGraphicsGetImageFromCurrentImageContext()

// End the image context to free up memory
UIGraphicsEndImageContext()

return circleImage
}

// Function to add a circular background with specified background color, diameter and padding
@objc func withCircularBackground(backgroundColor: UIColor, diameter: CGFloat, padding: CGFloat) -> UIImage? {
// Begin a new image context with the target diameter as both width and height
UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0.0)

// Define the circle's path using the diameter
let circlePath = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: diameter, height: diameter)))

// Set the fill color and fill the circle
backgroundColor.setFill()
circlePath.fill()

// Calculate the frame for the image inside the circle
let imageSize = CGSize(width: diameter - 2 * padding, height: diameter - 2 * padding)
let imageRect = CGRect(
x: (diameter - imageSize.width) / 2,
y: (diameter - imageSize.height) / 2,
width: imageSize.width,
height: imageSize.height
)

// Draw the image inside the calculated frame
self.draw(in: imageRect)

// Capture the final image
let resultImage = UIGraphicsGetImageFromCurrentImageContext()

// End the image context to free up memory
UIGraphicsEndImageContext()

return resultImage
}

// Function to create a UIImage from a UILabel
@objc static func image(from label: UILabel) -> UIImage? {
// Begin a new image context with the size of the label
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0)

// Render the label layer into the current context
label.layer.render(in: UIGraphicsGetCurrentContext()!)

// Capture the image from the context
let image = UIGraphicsGetImageFromCurrentImageContext()

// End the image context to free up memory
UIGraphicsEndImageContext()

return image
}
}
7 changes: 4 additions & 3 deletions NextcloudTalk/UserStatusSwiftUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SwiftUI
import SwiftUIIntrospect
@_spi(Advanced) import SwiftUIIntrospect

Check warning on line 9 in NextcloudTalk/UserStatusSwiftUIView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Duplicate Imports Violation: Imports should be unique (duplicate_imports)

protocol UserStatusViewDelegate: AnyObject {
@objc protocol UserStatusViewDelegate: AnyObject {
func userStatusViewDidDisappear()
}

Expand Down Expand Up @@ -99,8 +99,9 @@ struct UserStatusSwiftUIView: View {

@objc class UserStatusSwiftUIViewFactory: NSObject {

@objc static func create(userStatus: NCUserStatus) -> UIViewController {
let userStatusView = UserStatusSwiftUIView(userStatus: userStatus)
@objc static func create(userStatus: NCUserStatus, delegate: UserStatusViewDelegate) -> UIViewController {
var userStatusView = UserStatusSwiftUIView(userStatus: userStatus)
userStatusView.delegate = delegate
let hostingController = UIHostingController(rootView: userStatusView)

return hostingController
Expand Down

0 comments on commit c139836

Please sign in to comment.