diff --git a/NextcloudTalk/Calls/CallViewController.swift b/NextcloudTalk/Calls/CallViewController.swift index b701589eb..84ced6a24 100644 --- a/NextcloudTalk/Calls/CallViewController.swift +++ b/NextcloudTalk/Calls/CallViewController.swift @@ -195,6 +195,8 @@ class CallViewController: UIViewController, self.audioMuteButton.addGestureRecognizer(pushToTalkRecognizer) self.participantsLabelContainer.isHidden = true + let participantsLabelTapGesture = UITapGestureRecognizer(target: self, action: #selector(showParticipantsInRoomInfo)) + self.participantsLabelContainer.addGestureRecognizer(participantsLabelTapGesture) self.screensharingView.isHidden = true self.screensharingView.clipsToBounds = true @@ -1106,13 +1108,26 @@ class CallViewController: UIViewController, guard self.room.type != .oneToOne, let personImage = UIImage(systemName: "person.2") else { return } - DispatchQueue.main.async { - let participantAttachment = NSTextAttachment(image: personImage.withTintColor(self.participantsLabel.textColor)) - let participantText = NSMutableAttributedString(attachment: participantAttachment) - participantText.append(" \(self.peersInCall.count + 1)".withFont(self.participantsLabel.font)) + WebRTCCommon.shared.dispatch { + var participantsInCall: [TalkActor] = [] + + self.peersInCall.forEach { peerConnection in + let actor = self.callController?.getActorFromSessionId(peerConnection.peerId) ?? TalkActor() + participantsInCall.append(actor) + } + + let ownActor = TalkActor(actorId: self.room.account?.userId, actorType: kParticipantTypeUser) + participantsInCall.append(ownActor) - self.participantsLabel.attributedText = participantText - self.participantsLabelContainer.isHidden = false + DispatchQueue.main.async { + let participantAttachment = NSTextAttachment(image: personImage.withTintColor(self.participantsLabel.textColor)) + let participantText = NSMutableAttributedString(attachment: participantAttachment) + let uniqueParticipantsCount = Set(participantsInCall.map({ $0.id })).count + participantText.append(" \(uniqueParticipantsCount)".withFont(self.participantsLabel.font)) + + self.participantsLabel.attributedText = participantText + self.participantsLabelContainer.isHidden = false + } } } @@ -1991,6 +2006,28 @@ class CallViewController: UIViewController, } } + func showParticipantsInRoomInfo() { + self.showRoomInfo(scrollToParticipantsSection: true) + } + + func showRoomInfo(scrollToParticipantsSection: Bool = false) { + let roomInfoVC = RoomInfoUIViewFactory.create(room: self.room, showDestructiveActions: false, scrollToParticipantsSectionOnAppear: scrollToParticipantsSection) + roomInfoVC.modalPresentationStyle = .pageSheet + + let navController = UINavigationController(rootViewController: roomInfoVC) + let cancelButton = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { _ in + roomInfoVC.dismiss(animated: true) + }) + + if #unavailable(iOS 26.0) { + cancelButton.tintColor = NCAppBranding.themeTextColor() + } + + navController.navigationBar.topItem?.leftBarButtonItem = cancelButton + + self.present(navController, animated: true) + } + func showChat() { if chatNavigationController == nil { guard let room = NCDatabaseManager.sharedInstance().room(withToken: room.token, forAccountId: room.accountId), @@ -2451,20 +2488,6 @@ class CallViewController: UIViewController, // MARK: - NCChatTitleViewDelegate func chatTitleViewTapped(_ chatTitleView: NCChatTitleView?) { - let roomInfoVC = RoomInfoUIViewFactory.create(room: self.room, showDestructiveActions: false) - roomInfoVC.modalPresentationStyle = .pageSheet - - let navController = UINavigationController(rootViewController: roomInfoVC) - let cancelButton = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { _ in - roomInfoVC.dismiss(animated: true) - }) - - if #unavailable(iOS 26.0) { - cancelButton.tintColor = NCAppBranding.themeTextColor() - } - - navController.navigationBar.topItem?.leftBarButtonItem = cancelButton - - self.present(navController, animated: true) + showRoomInfo() } } diff --git a/NextcloudTalk/Rooms/RoomInfo/RoomInfoParticipantsSection.swift b/NextcloudTalk/Rooms/RoomInfo/RoomInfoParticipantsSection.swift index 3a9e98a93..26bd4f64d 100644 --- a/NextcloudTalk/Rooms/RoomInfo/RoomInfoParticipantsSection.swift +++ b/NextcloudTalk/Rooms/RoomInfo/RoomInfoParticipantsSection.swift @@ -36,12 +36,29 @@ struct RoomInfoParticipantsSection: View { } private var participantCountText: String { - return participants == nil ? "" : String.localizedStringWithFormat(NSLocalizedString("%ld participants", comment: ""), participants?.count ?? 0) + guard let participants else { return "" } + return String.localizedStringWithFormat(NSLocalizedString("%ld participants", comment: ""), participants.count) + } + + private var participantsInCallCountText: String { + return String.localizedStringWithFormat(NSLocalizedString("%ld participants in this call", comment: ""), participantsInCall.count) + } + + private var otherParticipansText: String { + guard let participants else { return "" } + if room.hasCall, !participantsInCall.isEmpty, participantsInCall.count == participants.count { + return "" + } + return String.localizedStringWithFormat(NSLocalizedString("Other participants", comment: "")) + } + + private var participantsInCall: [NCRoomParticipant] { + return participants?.filter { $0.inCall.contains(.inCall) } ?? [] } var body: (some View)? { if room.canAddParticipants { - Section(participantCountText) { + Section(NSLocalizedString("Conversation participants", comment: "")) { Button(action: addParticipants) { if room.type == .oneToOne { ImageSublabelView(image: Image(systemName: "person.badge.plus")) { @@ -59,71 +76,26 @@ struct RoomInfoParticipantsSection: View { } } - Section(room.canAddParticipants ? "" : participantCountText) { - if let participants = Binding($participants) { - ForEach(participants, id: \.self) { $participant in - Menu { - if room.canModerate, participant.canBeModerated { - if participant.canBeDemoted { - Button { - self.changeModerationPermission(forParticipant: participant, canModerate: false) - } label: { - Label(NSLocalizedString("Demote from moderator", comment: ""), systemImage: "person") - } - } - - if participant.canBePromoted { - Button { - self.changeModerationPermission(forParticipant: participant, canModerate: true) - } label: { - Label(NSLocalizedString("Promote to moderator", comment: ""), systemImage: "crown") - } - } - } - - if participant.canBeNotifiedAboutCall, room.permissions.contains(.startCall), room.participantFlags != [] { - Button { - self.sendCallNotification(forParticipant: participant) - } label: { - Label(NSLocalizedString("Send call notification", comment: ""), systemImage: "bell") - } - } - - if participant.actorType == .email { - Button { - self.resendInvitation(forParticipant: participant) - } label: { - Label(NSLocalizedString("Resend invitation", comment: ""), systemImage: "envelope") - } - } - - if room.canModerate, participant.canBeModerated { - if participant.canBeBanned { - Button(role: .destructive) { - participantToBan = participant - banConfirmationShown = true - } label: { - Label(NSLocalizedString("Ban participant", comment: ""), systemImage: "person.badge.minus") - } - .foregroundStyle(.primary) - .disabled(isBanActionRunning) - } - - Button(role: .destructive) { - Task { - await removeParticipant(participant: participant) - } - } label: { - Label(getRemoveLabel(forParticipant: participant), systemImage: "trash") - } + if room.hasCall { + Section(participantsInCallCountText) { + if let participants = Binding($participants) { + ForEach(participants, id: \.self) { $participant in + if participant.inCall.contains(.inCall) { + participantView(participant: $participant) } - } label: { - ContactsTableViewCellWrapper(room: $room, participant: $participant) - .frame(height: 72) // Height set in the XIB file } - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 12)) - .alignmentGuide(.listRowSeparatorLeading) { _ in - 72 + } else { + ProgressView() + .listRowInsets(nil) + } + } + } + + Section(room.hasCall ? otherParticipansText : participantCountText) { + if let participants = Binding($participants) { + ForEach(participants, id: \.self) { $participant in + if !room.hasCall || participant.inCall.isEmpty { + participantView(participant: $participant) } } } else { @@ -134,7 +106,6 @@ struct RoomInfoParticipantsSection: View { .task { getParticipants() } - .listRowInsets(room.canAddParticipants ? EdgeInsets(top: -12, leading: 0, bottom: 0, trailing: 0) : nil) .alert(String(format: NSLocalizedString("Ban %@", comment: "e.g. Ban John Doe"), participantToBan?.displayName ?? "Unknown"), isPresented: $banConfirmationShown) { // Can't move alert inside a menu element, it needs to be outside of the menu @@ -259,4 +230,71 @@ struct RoomInfoParticipantsSection: View { self.getParticipants() } } + + func participantView(participant: Binding) -> some View { + let wrappedParticipant = participant.wrappedValue + return Menu { + if room.canModerate, wrappedParticipant.canBeModerated { + if wrappedParticipant.canBeDemoted { + Button { + self.changeModerationPermission(forParticipant: wrappedParticipant, canModerate: false) + } label: { + Label(NSLocalizedString("Demote from moderator", comment: ""), systemImage: "person") + } + } + + if wrappedParticipant.canBePromoted { + Button { + self.changeModerationPermission(forParticipant: wrappedParticipant, canModerate: true) + } label: { + Label(NSLocalizedString("Promote to moderator", comment: ""), systemImage: "crown") + } + } + } + + if wrappedParticipant.canBeNotifiedAboutCall, room.permissions.contains(.startCall), room.participantFlags.contains(.inCall) { + Button { + self.sendCallNotification(forParticipant: wrappedParticipant) + } label: { + Label(NSLocalizedString("Send call notification", comment: ""), systemImage: "bell") + } + } + + if wrappedParticipant.actorType == .email { + Button { + self.resendInvitation(forParticipant: wrappedParticipant) + } label: { + Label(NSLocalizedString("Resend invitation", comment: ""), systemImage: "envelope") + } + } + + if room.canModerate, wrappedParticipant.canBeModerated { + if wrappedParticipant.canBeBanned { + Button(role: .destructive) { + participantToBan = wrappedParticipant + banConfirmationShown = true + } label: { + Label(NSLocalizedString("Ban participant", comment: ""), systemImage: "person.badge.minus") + } + .foregroundStyle(.primary) + .disabled(isBanActionRunning) + } + + Button(role: .destructive) { + Task { + await removeParticipant(participant: wrappedParticipant) + } + } label: { + Label(getRemoveLabel(forParticipant: wrappedParticipant), systemImage: "trash") + } + } + } label: { + ContactsTableViewCellWrapper(room: $room, participant: participant) + .frame(height: 72) // Height set in the XIB file + } + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 12)) + .alignmentGuide(.listRowSeparatorLeading) { _ in + 72 + } + } } diff --git a/NextcloudTalk/Rooms/RoomInfo/RoomInfoSwiftUIView.swift b/NextcloudTalk/Rooms/RoomInfo/RoomInfoSwiftUIView.swift index 98b8218e5..3be5682ab 100644 --- a/NextcloudTalk/Rooms/RoomInfo/RoomInfoSwiftUIView.swift +++ b/NextcloudTalk/Rooms/RoomInfo/RoomInfoSwiftUIView.swift @@ -21,6 +21,7 @@ class HostingControllerWrapper { struct RoomInfoSwiftUIView: View { let hostingWrapper: HostingControllerWrapper + let scrollToParticipantsSectionOnAppear: Bool @State var room: NCRoom @State var showDestructiveActions: Bool = true @@ -28,26 +29,32 @@ struct RoomInfoSwiftUIView: View { @State var profileInfo: ProfileInfo? var body: some View { - List { - RoomInfoHeaderSection(hostingWrapper: hostingWrapper, room: $room, profileInfo: $profileInfo) + ScrollViewReader { proxy in + List { + RoomInfoHeaderSection(hostingWrapper: hostingWrapper, room: $room, profileInfo: $profileInfo) - RoomInfoFileSection(hostingWrapper: hostingWrapper, room: $room, quickLookUrl: $quickLookUrl) - RoomInfoSharedItemsSection(hostingWrapper: hostingWrapper, room: $room) + RoomInfoFileSection(hostingWrapper: hostingWrapper, room: $room, quickLookUrl: $quickLookUrl) + RoomInfoSharedItemsSection(hostingWrapper: hostingWrapper, room: $room) - RoomInfoNotificationSection(room: $room) - RoomInfoConversationSettingsSection(hostingWrapper: hostingWrapper, room: $room) + RoomInfoNotificationSection(room: $room) + RoomInfoConversationSettingsSection(hostingWrapper: hostingWrapper, room: $room) - RoomInfoGuestSection(room: $room) - RoomInfoWebinarSection(room: $room) - RoomInfoSIPInfoSection(room: $room) + RoomInfoGuestSection(room: $room) + RoomInfoWebinarSection(room: $room) + RoomInfoSIPInfoSection(room: $room) - RoomInfoParticipantsSection(hostingWrapper: hostingWrapper, room: $room) + RoomInfoParticipantsSection(hostingWrapper: hostingWrapper, room: $room).id("participantsSection") - RoomInfoNonDestructiveSection(room: $room) + RoomInfoNonDestructiveSection(room: $room) - if showDestructiveActions { - RoomInfoDestructiveSection(room: $room) - } + if showDestructiveActions { + RoomInfoDestructiveSection(room: $room) + } + }.onAppear(perform: { + if scrollToParticipantsSectionOnAppear { + proxy.scrollTo("participantsSection", anchor: .center) + } + }) } .quickLookPreview($quickLookUrl) .environment(\.defaultMinListHeaderHeight, 1) @@ -77,9 +84,9 @@ struct RoomInfoSwiftUIView: View { @objc class RoomInfoUIViewFactory: NSObject { - @objc static func create(room: NCRoom, showDestructiveActions: Bool) -> UIViewController { + @objc static func create(room: NCRoom, showDestructiveActions: Bool, scrollToParticipantsSectionOnAppear: Bool = false) -> UIViewController { let wrapper = HostingControllerWrapper() - let roomInfoView = RoomInfoSwiftUIView(hostingWrapper: wrapper, room: room, showDestructiveActions: showDestructiveActions) + let roomInfoView = RoomInfoSwiftUIView(hostingWrapper: wrapper, scrollToParticipantsSectionOnAppear: scrollToParticipantsSectionOnAppear, room: room, showDestructiveActions: showDestructiveActions) let hostingController = UIHostingController(rootView: roomInfoView) hostingController.title = NSLocalizedString("Conversation settings", comment: "") NCAppBranding.styleViewController(hostingController) diff --git a/NextcloudTalk/Rooms/RoomsTableViewController.m b/NextcloudTalk/Rooms/RoomsTableViewController.m index d4b632060..88a84cdd9 100644 --- a/NextcloudTalk/Rooms/RoomsTableViewController.m +++ b/NextcloudTalk/Rooms/RoomsTableViewController.m @@ -1324,7 +1324,7 @@ - (void)removeRoomFromFavorites:(NCRoom *)room - (void)presentRoomInfoForRoom:(NCRoom *)room { - UIViewController *roomInfoVC = [RoomInfoUIViewFactory createWithRoom:room showDestructiveActions:YES]; + UIViewController *roomInfoVC = [RoomInfoUIViewFactory createWithRoom:room showDestructiveActions:YES scrollToParticipantsSectionOnAppear:NO]; NCNavigationController *navigationController = [[NCNavigationController alloc] initWithRootViewController:roomInfoVC]; UIAction *cancelAction = [UIAction actionWithHandler:^(__kindof UIAction * _Nonnull action) { diff --git a/NextcloudTalk/en.lproj/Localizable.strings b/NextcloudTalk/en.lproj/Localizable.strings index d433ee7dc..1039a68bf 100644 --- a/NextcloudTalk/en.lproj/Localizable.strings +++ b/NextcloudTalk/en.lproj/Localizable.strings @@ -67,6 +67,9 @@ /* No comment provided by engineer. */ "%ld participants" = "%ld participants"; +/* No comment provided by engineer. */ +"%ld participants in this call" = "%ld participants in this call"; + /* Replies in a thread */ "%ld replies" = "%ld replies"; @@ -607,6 +610,9 @@ /* No comment provided by engineer. */ "Conversation not found or not joined" = "Conversation not found or not joined"; +/* No comment provided by engineer. */ +"Conversation participants" = "Conversation participants"; + /* No comment provided by engineer. */ "Conversation settings" = "Conversation settings"; @@ -1616,6 +1622,9 @@ /* No comment provided by engineer. */ "Other Accounts" = "Other Accounts"; +/* No comment provided by engineer. */ +"Other participants" = "Other participants"; + /* No comment provided by engineer. */ "Others" = "Others"; diff --git a/NextcloudTalk/en.lproj/Localizable.stringsdict b/NextcloudTalk/en.lproj/Localizable.stringsdict index aa2fbcfb5..886d8ba79 100644 --- a/NextcloudTalk/en.lproj/Localizable.stringsdict +++ b/NextcloudTalk/en.lproj/Localizable.stringsdict @@ -36,6 +36,25 @@ + %ld participants in this call + + NSStringLocalizedFormatKey + %#@participants@ + participants + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld participant in this call + many + %ld participants in this call + other + %ld participants in this call + + + %d minutes ago NSStringLocalizedFormatKey