Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User status in settings button #1793

Merged
merged 3 commits into from
Sep 18, 2024
Merged
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
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 @@ -6,13 +6,13 @@
import UIKit
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()
}

struct UserStatusSwiftUIView: View {

Check warning on line 15 in NextcloudTalk/UserStatusSwiftUIView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

File Types Order Violation: A 'main_type' should not be placed amongst the file type(s) 'supporting_type' (file_types_order)

@Environment(\.dismiss) var dismiss
@State var userStatus: NCUserStatus
Expand Down Expand Up @@ -53,7 +53,7 @@
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}) {

Check warning on line 56 in NextcloudTalk/UserStatusSwiftUIView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Multiple Closures with Trailing Closure Violation: Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
Text("Cancel")
.foregroundColor(Color(NCAppBranding.themeTextColor()))
}
Expand Down Expand Up @@ -99,8 +99,9 @@

@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
Loading