From 4512057eff99940c250a9cf5d1542579480c27d0 Mon Sep 17 00:00:00 2001 From: Ivan Sein Date: Thu, 5 Sep 2024 16:11:50 +0200 Subject: [PATCH 1/3] Show user status in settings button. Signed-off-by: Ivan Sein --- NextcloudTalk.xcodeproj/project.pbxproj | 4 + NextcloudTalk/NCUserStatus.h | 1 + NextcloudTalk/NCUserStatus.m | 8 ++ NextcloudTalk/RoomsTableViewController.m | 73 +++++++++++++--- NextcloudTalk/UIImageExtension.swift | 106 +++++++++++++++++++++++ 5 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 NextcloudTalk/UIImageExtension.swift diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index 8833b2f11..296abfdaf 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -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 */; }; @@ -1110,6 +1111,7 @@ 2CB6ACE82641954700D3D641 /* MapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MapViewController.xib; sourceTree = ""; }; 2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiAvatarPickerViewController.swift; sourceTree = ""; }; 2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmojiAvatarPickerViewController.xib; sourceTree = ""; }; + 2CBD0D592C8770A40013C089 /* UIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; 2CBF82AC1FC888FC00636459 /* NCPushNotification.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCPushNotification.h; sourceTree = ""; }; 2CBF82AD1FC888FC00636459 /* NCPushNotification.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCPushNotification.m; sourceTree = ""; }; 2CBF82B01FCC7DBA00636459 /* CCCertificate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CCCertificate.h; sourceTree = ""; }; @@ -1612,6 +1614,7 @@ 2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */, 1FAB2EED2AD1BC1B001214EB /* UIControlExtensions.swift */, 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */, + 2CBD0D592C8770A40013C089 /* UIImageExtension.swift */, 1F1B0F312BDC57E3003FD766 /* UIPageViewControllerExtension.swift */, 1F1B0F352BDD8B9C003FD766 /* NCActivityIndicator.swift */, ); @@ -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 */, diff --git a/NextcloudTalk/NCUserStatus.h b/NextcloudTalk/NCUserStatus.h index 0d102460c..375fdf698 100644 --- a/NextcloudTalk/NCUserStatus.h +++ b/NextcloudTalk/NCUserStatus.h @@ -35,6 +35,7 @@ extern NSString * const kUserStatusOffline; - (NSString *)readableUserStatusMessage; - (NSString *)readableUserStatusOrMessage; - (nullable UIImage *)getSFUserStatusIcon; +- (BOOL)hasVisibleStatusIcon; @end diff --git a/NextcloudTalk/NCUserStatus.m b/NextcloudTalk/NCUserStatus.m index 57e31c85a..f7f21d17a 100644 --- a/NextcloudTalk/NCUserStatus.m +++ b/NextcloudTalk/NCUserStatus.m @@ -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 diff --git a/NextcloudTalk/RoomsTableViewController.m b/NextcloudTalk/RoomsTableViewController.m index baf79df92..c79810b13 100644 --- a/NextcloudTalk/RoomsTableViewController.m +++ b/NextcloudTalk/RoomsTableViewController.m @@ -56,6 +56,7 @@ @interface RoomsTableViewController () 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 diff --git a/NextcloudTalk/UIImageExtension.swift b/NextcloudTalk/UIImageExtension.swift new file mode 100644 index 000000000..fcd669348 --- /dev/null +++ b/NextcloudTalk/UIImageExtension.swift @@ -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 + } +} From c3508dcb79436d1e8c86d5457787941f31636397 Mon Sep 17 00:00:00 2001 From: Ivan Sein Date: Thu, 5 Sep 2024 21:16:31 +0200 Subject: [PATCH 2/3] Implement UserStatusSwiftUIView delegate in RoomsTableViewController. Signed-off-by: Ivan Sein --- NextcloudTalk/RoomsTableViewController.m | 11 +++++++++-- NextcloudTalk/UserStatusSwiftUIView.swift | 7 ++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/NextcloudTalk/RoomsTableViewController.m b/NextcloudTalk/RoomsTableViewController.m index c79810b13..509dd1a8f 100644 --- a/NextcloudTalk/RoomsTableViewController.m +++ b/NextcloudTalk/RoomsTableViewController.m @@ -41,7 +41,7 @@ kRoomsSectionRoomList } RoomsSections; -@interface RoomsTableViewController () +@interface RoomsTableViewController () { RLMNotificationToken *_rlmNotificationToken; NSMutableArray *_rooms; @@ -442,6 +442,13 @@ - (void)refreshControlTarget AudioServicesPlaySystemSound(1519); } +#pragma mark - User Status SwiftUI View Delegate + +- (void)userStatusViewDidDisappear +{ + [self getUserStatusWithCompletionBlock:nil]; +} + #pragma mark - Title menu - (void)setNavigationLogoButton @@ -471,7 +478,7 @@ - (UIMenu *)getActiveAccountMenuOptions 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]; diff --git a/NextcloudTalk/UserStatusSwiftUIView.swift b/NextcloudTalk/UserStatusSwiftUIView.swift index 00b5dc194..c3b328110 100644 --- a/NextcloudTalk/UserStatusSwiftUIView.swift +++ b/NextcloudTalk/UserStatusSwiftUIView.swift @@ -8,7 +8,7 @@ import SwiftUI import SwiftUIIntrospect @_spi(Advanced) import SwiftUIIntrospect -protocol UserStatusViewDelegate: AnyObject { +@objc protocol UserStatusViewDelegate: AnyObject { func userStatusViewDidDisappear() } @@ -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 From 7252a9a35bab3d782aef30407800e1fcb4343793 Mon Sep 17 00:00:00 2001 From: Ivan Sein Date: Thu, 5 Sep 2024 21:22:36 +0200 Subject: [PATCH 3/3] Clear active user status when changing users. Signed-off-by: Ivan Sein --- NextcloudTalk/RoomsTableViewController.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NextcloudTalk/RoomsTableViewController.m b/NextcloudTalk/RoomsTableViewController.m index 509dd1a8f..075e8bc23 100644 --- a/NextcloudTalk/RoomsTableViewController.m +++ b/NextcloudTalk/RoomsTableViewController.m @@ -809,6 +809,8 @@ - (void)adaptInterfaceForAppState:(AppState)appState case kAppStateMissingServerCapabilities: case kAppStateMissingSignalingConfiguration: { + // Clear active user status when changing users + _activeUserStatus = nil; [self setProfileButton]; } break;