diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 4cc29b46e..a472253e4 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071D2AC5922E0037746F /* ColorExtension.swift */; }; D71707202AC5989C0037746F /* TextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071F2AC5989C0037746F /* TextExtension.swift */; }; D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */; }; + D717630D2BD7BD0E00464097 /* ParticipantsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717630C2BD7BD0E00464097 /* ParticipantsListFragment.swift */; }; D71968922B86369D00DF4459 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71968912B86369D00DF4459 /* ChatBubbleView.swift */; }; D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */; }; D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABB82ABC67BF00B41C10 /* ContentView.swift */; }; @@ -191,6 +192,7 @@ D717071D2AC5922E0037746F /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; D717071F2AC5989C0037746F /* TextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextExtension.swift; sourceTree = ""; }; D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneUtils.swift; sourceTree = ""; }; + D717630C2BD7BD0E00464097 /* ParticipantsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsListFragment.swift; sourceTree = ""; }; D71968912B86369D00DF4459 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; D719ABB32ABC67BF00B41C10 /* Linphone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Linphone.app; sourceTree = BUILT_PRODUCTS_DIR; }; D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneApp.swift; sourceTree = ""; }; @@ -582,6 +584,7 @@ children = ( D75759312B56D40900E7AC10 /* ZRTPPopup.swift */, D7F4D9CA2B5FD27200CDCD76 /* CallsListFragment.swift */, + D717630C2BD7BD0E00464097 /* ParticipantsListFragment.swift */, ); path = Fragments; sourceTree = ""; @@ -954,6 +957,7 @@ D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, 66E50A492BD12B2300AD61CA /* MeetingsView.swift in Sources */, D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, + D717630D2BD7BD0E00464097 /* ParticipantsListFragment.swift in Sources */, D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */, D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index c9993f7f4..d26d8e3f5 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -51,6 +51,16 @@ }, "%lld" : { + }, + "%lld %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld %2$@" + } + } + } }, "%lld Book (Example)" : { "extractionState" : "manual", @@ -167,6 +177,9 @@ }, "Add to favourites" : { + }, + "Administrateur" : { + }, "All calls will be removed from the history." : { @@ -355,6 +368,9 @@ }, "Error Name" : { + }, + "Etes-vous sûr de vouloir supprimer %@ ?" : { + }, "Favourites" : { @@ -509,6 +525,12 @@ }, "No meeting for the moment..." : { + }, + "No participant for the moment..." : { + + }, + "Non" : { + }, "Not account yet?" : { @@ -524,6 +546,9 @@ }, "Other actions" : { + }, + "Oui" : { + }, "Partage d'écran" : { @@ -683,6 +708,9 @@ }, "Supprimer la conversation" : { + }, + "Supprimer un participant" : { + }, "TCP" : { diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 250f4cdb1..a874bf3c7 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -50,7 +50,8 @@ struct CallView: View { @State var displayVideo = false @Binding var fullscreenVideo: Bool - @Binding var isShowCallsListFragment: Bool + @State var isShowCallsListFragment: Bool = false + @State var isShowParticipantsListFragment: Bool = false @Binding var isShowStartCallFragment: Bool var body: some View { @@ -107,8 +108,14 @@ struct CallView: View { if isShowCallsListFragment { CallsListFragment(callViewModel: callViewModel, isShowCallsListFragment: $isShowCallsListFragment) - .zIndex(4) - .transition(.move(edge: .bottom)) + .zIndex(4) + .transition(.move(edge: .bottom)) + } + + if isShowParticipantsListFragment { + ParticipantsListFragment(callViewModel: callViewModel, isShowParticipantsListFragment: $isShowParticipantsListFragment) + .zIndex(4) + .transition(.move(edge: .bottom)) } if callViewModel.zrtpPopupDisplayed == true { @@ -1218,6 +1225,9 @@ struct CallView: View { VStack { Button { + withAnimation { + isShowParticipantsListFragment.toggle() + } } label: { HStack { Image("users") @@ -1458,58 +1468,110 @@ struct CallView: View { .frame(height: geo.size.height * 0.15) } else { HStack { - VStack { - Button { - withAnimation { - callViewModel.isTransferInsteadCall = true - MagicSearchSingleton.shared.searchForSuggestions() - isShowStartCallFragment.toggle() + if callViewModel.isOneOneCall { + VStack { + Button { + withAnimation { + callViewModel.isTransferInsteadCall = true + MagicSearchSingleton.shared.searchForSuggestions() + isShowStartCallFragment.toggle() + } + } label: { + HStack { + Image("phone-transfer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } } - } label: { - HStack { - Image("phone-transfer") - .renderingMode(.template) - .resizable() + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text(callViewModel.calls.count < 2 ? "Transfer" : "Attended transfer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + + VStack { + Button { + withAnimation { + MagicSearchSingleton.shared.searchForSuggestions() + isShowStartCallFragment.toggle() + } + } label: { + HStack { + Image("phone-plus") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("New call") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + } else { + VStack { + VStack { + Button { + } label: { + HStack { + Image("screencast") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.gray500) + .frame(width: 32, height: 32) + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(.white) + .cornerRadius(40) + .disabled(true) + + Text("Partage d'écran") .foregroundStyle(.white) - .frame(width: 32, height: 32) + .default_text_style(styleSize: 15) } } - .buttonStyle(PressedButtonStyle()) - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) - Text(callViewModel.calls.count < 2 ? "Transfer" : "Attended transfer") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) - - VStack { - Button { - withAnimation { - MagicSearchSingleton.shared.searchForSuggestions() - isShowStartCallFragment.toggle() - } - } label: { - HStack { - Image("phone-plus") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) + VStack { + Button { + withAnimation { + isShowParticipantsListFragment.toggle() + } + } label: { + HStack { + Image("users") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Participants") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) } - .buttonStyle(PressedButtonStyle()) - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("New call") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) } - .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) VStack { ZStack { @@ -1823,7 +1885,7 @@ struct PressedButtonStyle: ButtonStyle { } #Preview { - CallView(callViewModel: CallViewModel(), fullscreenVideo: .constant(false), isShowCallsListFragment: .constant(false), isShowStartCallFragment: .constant(false)) + CallView(callViewModel: CallViewModel(), fullscreenVideo: .constant(false), isShowStartCallFragment: .constant(false)) } // swiftlint:enable type_body_length // swiftlint:enable line_length diff --git a/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift b/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift index 100b7d882..301d1de99 100644 --- a/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift +++ b/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift @@ -1,18 +1,239 @@ -// -// ParticipantsListFragment.swift -// Linphone -// -// Created by Benoît Martins on 23/04/2024. -// +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import SwiftUI struct ParticipantsListFragment: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } + + @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var contactsManager = ContactsManager.shared + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject var callViewModel: CallViewModel + + @State private var delayedColor = Color.white + + @Binding var isShowParticipantsListFragment: Bool + + @State private var isShowPopup = false + @State private var indexToRemove = -1 + + var body: some View { + ZStack { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(delayedColor) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + .task(delayColor) + + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 2) + .padding(.leading, -10) + .onTapGesture { + delayColorDismiss() + withAnimation { + isShowParticipantsListFragment.toggle() + } + } + + Text("\(callViewModel.participantList.count + 1) \(callViewModel.participantList.isEmpty ? "Participant" : "Participants")") + .multilineTextAlignment(.leading) + .default_text_style_orange_800(styleSize: 16) + + Spacer() + + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + participantsList + } + .background(.white) + + if self.isShowPopup { + let contentPopup = Text("Etes-vous sûr de vouloir supprimer \(callViewModel.participantList[indexToRemove].name) ?") + PopupView(isShowPopup: $isShowPopup, + title: Text("Supprimer un participant"), + content: contentPopup, + titleFirstButton: Text("Non"), + actionFirstButton: {self.isShowPopup.toggle()}, + titleSecondButton: Text("Oui"), + actionSecondButton: { + callViewModel.removeParticipant(index: indexToRemove) + self.isShowPopup.toggle() + indexToRemove = -1 + }) + .background(.black.opacity(0.65)) + .onTapGesture { + self.isShowPopup.toggle() + indexToRemove = -1 + } + } + } + .navigationBarHidden(true) + } + + @Sendable private func delayColor() async { + try? await Task.sleep(nanoseconds: 250_000_000) + delayedColor = Color.orangeMain500 + } + + func delayColorDismiss() { + Task { + try? await Task.sleep(nanoseconds: 80_000_000) + delayedColor = .white + } + } + + var participantsList: some View { + VStack { + List { + HStack { + HStack { + if callViewModel.myParticipantModel != nil { + Avatar(contactAvatarModel: callViewModel.myParticipantModel!.avatarModel, avatarSize: 50, hidePresence: true) + + Text(callViewModel.myParticipantModel!.name) + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + + Spacer() + + if callViewModel.myParticipantModel!.isAdmin { + Text("Administrateur") + .foregroundStyle(Color.grayMain2c300) + .default_text_style(styleSize: 12) + .frame(maxWidth: .infinity, alignment: .trailing) + .lineLimit(1) + } + + if callViewModel.myParticipantModel!.isAdmin { + Toggle("", isOn: .constant(true)) + .tint(Color.greenSuccess700) + .labelsHidden() + .padding(.horizontal, 4) + + HStack(alignment: .center, spacing: 10) { + Image("x") + .renderingMode(.template) + .foregroundStyle(Color.grayMain2c400) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(width: 30, height: 30, alignment: .center) + .hidden() + } + } + } + } + .buttonStyle(.borderless) + .listRowInsets(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)) + .listRowSeparator(.hidden) + .background(.white) + + ForEach(0..