From baf1fcc0b9b1b8d1858b7ee01c8cdee3dbe3b24f Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 4 Nov 2024 15:37:06 +0100 Subject: [PATCH] Add Conversation info fragment --- Linphone.xcodeproj/project.pbxproj | 4 + Linphone/Localizable.xcstrings | 119 ++++ .../Fragments/ConversationFragment.swift | 585 +++++++++--------- .../Fragments/ConversationInfoFragment.swift | 369 +++++++++++ .../Fragments/ConversationsFragment.swift | 6 +- .../ConversationsListBottomSheet.swift | 233 +++---- .../Model/ConversationModel.swift | 2 +- 7 files changed, 924 insertions(+), 394 deletions(-) create mode 100644 Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 07e493978..e1c59af06 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -130,6 +130,7 @@ D78E06302BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E062F2BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift */; }; D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79622332B1DFE600037EACD /* DialerBottomSheet.swift */; }; D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */; }; + D79F1C162CD3D6AD00FF0A05 /* ConversationInfoFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79F1C152CD3D6AD00FF0A05 /* ConversationInfoFragment.swift */; }; D79F2D0A2C47F4BF0038FA07 /* TouchFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79F2D092C47F4BF0038FA07 /* TouchFeedback.swift */; }; D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */; }; D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBF2ACC2E390081A588 /* HistoryView.swift */; }; @@ -319,6 +320,7 @@ D78E062F2BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLayoutBottomSheet.swift; sourceTree = ""; }; D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = ""; }; D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; + D79F1C152CD3D6AD00FF0A05 /* ConversationInfoFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationInfoFragment.swift; sourceTree = ""; }; D79F2D092C47F4BF0038FA07 /* TouchFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchFeedback.swift; sourceTree = ""; }; D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; @@ -857,6 +859,7 @@ D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */, D7A0ACBA2C415D630043AE79 /* StartGroupConversationFragment.swift */, D70C82A42C85EDC90087F43F /* ConversationForwardMessageFragment.swift */, + D79F1C152CD3D6AD00FF0A05 /* ConversationInfoFragment.swift */, ); path = Fragments; sourceTree = ""; @@ -1127,6 +1130,7 @@ D71556362C297DB1009A8CEF /* StartGroupCallFragment.swift in Sources */, C6A5A9452C10B6270070FEA4 /* OIDAuthStateExtension.swift in Sources */, D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */, + D79F1C162CD3D6AD00FF0A05 /* ConversationInfoFragment.swift in Sources */, D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */, C67586B02C09F247002E77BF /* URIHandler.swift in Sources */, C62817282C1B389700DBA646 /* SideMenuAccountRow.swift in Sources */, diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 1dd997c28..5490add1f 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -874,6 +874,23 @@ }, "Connexion à la réunion" : { + }, + "contact_details_actions_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Other actions" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autres actions" + } + } + } }, "contact_dialog_pick_phone_number_or_sip_address_title" : { "extractionState" : "manual", @@ -928,6 +945,57 @@ } } }, + "conversation_action_call" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Call" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appeler" + } + } + } + }, + "conversation_action_configure_ephemeral_messages" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure ephemeral messages" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurer les messages éphémères" + } + } + } + }, + "conversation_action_leave_group" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leave the group" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitter le groupe" + } + } + } + }, "conversation_action_mute" : { "extractionState" : "manual", "localizations" : { @@ -1432,6 +1500,40 @@ } } }, + "conversation_info_delete_history_action" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete history" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer l'historique" + } + } + } + }, + "conversation_info_menu_go_to_contact" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "See contact profile" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voir le contact" + } + } + } + }, "conversation_invalid_participant_due_to_security_mode_toast" : { "extractionState" : "manual", "localizations" : { @@ -2068,6 +2170,23 @@ }, "Meeting added to iPhone calendar" : { + }, + "meeting_schedule_meeting_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meeting" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réunion" + } + } + } }, "meeting_waiting_room_join" : { "extractionState" : "manual", diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 57f6c98b1..611b9a48c 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -56,6 +56,7 @@ struct ConversationFragment: View { @State private var isShowConversationForwardMessageFragment = false @State private var isShowEphemeralFragment = false + @State private var isShowInfoConversationFragment = false @Binding var isShowConversationFragment: Bool @Binding var isShowStartCallGroupPopup: Bool @@ -231,29 +232,40 @@ struct ConversationFragment: View { } } } + .background(.white) + .onTapGesture { + withAnimation { + isShowInfoConversationFragment = true + } + } .padding(.vertical, 10) Spacer() - Button { - if conversationViewModel.displayedConversation!.isGroup { - isShowStartCallGroupPopup.toggle() - } else { - conversationViewModel.displayedConversation!.call() + if !conversationViewModel.displayedConversation!.isReadOnly { + Button { + if conversationViewModel.displayedConversation!.isGroup { + isShowStartCallGroupPopup.toggle() + } else { + conversationViewModel.displayedConversation!.call() + } + } label: { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) } - } label: { - Image("phone") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - .padding(.top, 4) } Menu { Button { isMenuOpen = false + withAnimation { + isShowInfoConversationFragment = true + } } label: { HStack { Text("conversation_menu_go_to_info") @@ -267,38 +279,40 @@ struct ConversationFragment: View { } } - Button { - isMenuOpen = false - conversationViewModel.displayedConversation!.toggleMute() - isMuted = !isMuted - } label: { - HStack { - Text(isMuted ? "conversation_action_unmute" : "conversation_action_mute") - Spacer() - Image(isMuted ? "bell-simple" : "bell-simple-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) + if !conversationViewModel.displayedConversation!.isReadOnly { + Button { + isMenuOpen = false + conversationViewModel.displayedConversation!.toggleMute() + isMuted = !isMuted + } label: { + HStack { + Text(isMuted ? "conversation_action_unmute" : "conversation_action_mute") + Spacer() + Image(isMuted ? "bell-simple" : "bell-simple-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } } - } - - Button { - isMenuOpen = false - withAnimation { - isShowEphemeralFragment = true - } - } label: { - HStack { - Text("conversation_menu_configure_ephemeral_messages") - Spacer() - Image("clock-countdown") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) + + Button { + isMenuOpen = false + withAnimation { + isShowEphemeralFragment = true + } + } label: { + HStack { + Text("conversation_menu_configure_ephemeral_messages") + Spacer() + Image("clock-countdown") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } } } } label: { @@ -454,129 +468,42 @@ struct ConversationFragment: View { .transition(.move(edge: .bottom)) } - if conversationViewModel.messageToReply != nil { - ZStack(alignment: .top) { - HStack { - VStack { - ( - Text("conversation_reply_to_message_title") - + Text("**\(conversationViewModel.participantConversationModel.first(where: {$0.address == conversationViewModel.messageToReply!.message.address})?.name ?? "")**")) - .default_text_style_300(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, 1) - .lineLimit(1) - - if conversationViewModel.messageToReply!.message.text.isEmpty { - Text(conversationViewModel.messageToReply!.message.attachmentsNames) - .default_text_style_300(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) - } else { - Text("\(conversationViewModel.messageToReply!.message.text)") - .default_text_style_300(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) - } - } - } - .frame(maxWidth: .infinity) - .padding(.all, 20) - .background(Color.gray100) - - HStack { - Spacer() - - Button(action: { - withAnimation { - conversationViewModel.messageToReply = nil - } - }, label: { - Image("x") - .resizable() - .frame(width: 30, height: 30, alignment: .leading) - .padding(.all, 10) - }) - } - } - .transition(.move(edge: .bottom)) - } - - if !conversationViewModel.mediasToSend.isEmpty || mediasIsLoading { - ZStack(alignment: .top) { - HStack { - if mediasIsLoading { - HStack { - Spacer() + if conversationViewModel.displayedConversation != nil && !conversationViewModel.displayedConversation!.isReadOnly { + if conversationViewModel.messageToReply != nil { + ZStack(alignment: .top) { + HStack { + VStack { + ( + Text("conversation_reply_to_message_title") + + Text("**\(conversationViewModel.participantConversationModel.first(where: {$0.address == conversationViewModel.messageToReply!.message.address})?.name ?? "")**")) + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 1) + .lineLimit(1) - ProgressView() - - Spacer() - } - .frame(height: 120) - } - - if !mediasIsLoading { - LazyVGrid(columns: [ - GridItem(.adaptive(minimum: 100), spacing: 1) - ], spacing: 3) { - ForEach(conversationViewModel.mediasToSend, id: \.id) { attachment in - ZStack { - Rectangle() - .fill(Color(.white)) - .frame(width: 100, height: 100) - - AsyncImage(url: attachment.thumbnail) { image in - ZStack { - image - .resizable() - .interpolation(.medium) - .aspectRatio(contentMode: .fill) - - if attachment.type == .video { - Image("play-fill") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 40, height: 40, alignment: .leading) - } - } - } placeholder: { - ProgressView() - } - .layoutPriority(-1) - .onTapGesture { - if conversationViewModel.mediasToSend.count == 1 { - withAnimation { - conversationViewModel.mediasToSend.removeAll() - } - } else { - guard let index = self.conversationViewModel.mediasToSend.firstIndex(of: attachment) else { return } - self.conversationViewModel.mediasToSend.remove(at: index) - } - } - } - .clipShape(RoundedRectangle(cornerRadius: 4)) - .contentShape(Rectangle()) + if conversationViewModel.messageToReply!.message.text.isEmpty { + Text(conversationViewModel.messageToReply!.message.attachmentsNames) + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + Text("\(conversationViewModel.messageToReply!.message.text)") + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) } } - .frame( - width: geometry.size.width > 0 && CGFloat(102 * conversationViewModel.mediasToSend.count) > geometry.size.width - 20 - ? 102 * floor(CGFloat(geometry.size.width - 20) / 102) - : CGFloat(102 * conversationViewModel.mediasToSend.count) - ) } - } - .frame(maxWidth: .infinity) - .padding(.all, conversationViewModel.mediasToSend.isEmpty ? 0 : 10) - .background(Color.gray100) - - if !mediasIsLoading { + .frame(maxWidth: .infinity) + .padding(.all, 20) + .background(Color.gray100) + HStack { Spacer() Button(action: { withAnimation { - conversationViewModel.mediasToSend.removeAll() + conversationViewModel.messageToReply = nil } }, label: { Image("x") @@ -586,145 +513,234 @@ struct ConversationFragment: View { }) } } + .transition(.move(edge: .bottom)) } - .transition(.move(edge: .bottom)) - } - - HStack(spacing: 0) { - if !voiceRecordingInProgress { - Button { - } label: { - Image("smiley") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 28, height: 28, alignment: .leading) - .padding(.all, 6) - .padding(.top, 4) - } - .padding(.horizontal, isMessageTextFocused ? 0 : 2) - - Button { - self.isShowPhotoLibrary = true - self.mediasIsLoading = true - } label: { - Image("paperclip") - .renderingMode(.template) - .resizable() - .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) - .frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading) - .padding(.all, isMessageTextFocused ? 0 : 6) - .padding(.top, 4) - .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) - } - .padding(.horizontal, isMessageTextFocused ? 0 : 2) - - Button { - self.isShowCamera = true - } label: { - Image("camera") - .renderingMode(.template) - .resizable() - .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) - .frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading) - .padding(.all, isMessageTextFocused ? 0 : 6) - .padding(.top, 4) - .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) - } - .padding(.horizontal, isMessageTextFocused ? 0 : 2) - - HStack { - if #available(iOS 16.0, *) { - TextField("Say something...", text: $conversationViewModel.messageText, axis: .vertical) - .default_text_style(styleSize: 15) - .focused($isMessageTextFocused) - .padding(.vertical, 5) - .onChange(of: conversationViewModel.messageText) { text in - if !text.isEmpty && !CoreContext.shared.enteredForeground { - conversationViewModel.compose() + + if !conversationViewModel.mediasToSend.isEmpty || mediasIsLoading { + ZStack(alignment: .top) { + HStack { + if mediasIsLoading { + HStack { + Spacer() + + ProgressView() + + Spacer() + } + .frame(height: 120) + } + + if !mediasIsLoading { + LazyVGrid(columns: [ + GridItem(.adaptive(minimum: 100), spacing: 1) + ], spacing: 3) { + ForEach(conversationViewModel.mediasToSend, id: \.id) { attachment in + ZStack { + Rectangle() + .fill(Color(.white)) + .frame(width: 100, height: 100) + + AsyncImage(url: attachment.thumbnail) { image in + ZStack { + image + .resizable() + .interpolation(.medium) + .aspectRatio(contentMode: .fill) + + if attachment.type == .video { + Image("play-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 40, height: 40, alignment: .leading) + } + } + } placeholder: { + ProgressView() + } + .layoutPriority(-1) + .onTapGesture { + if conversationViewModel.mediasToSend.count == 1 { + withAnimation { + conversationViewModel.mediasToSend.removeAll() + } + } else { + guard let index = self.conversationViewModel.mediasToSend.firstIndex(of: attachment) else { return } + self.conversationViewModel.mediasToSend.remove(at: index) + } + } + } + .clipShape(RoundedRectangle(cornerRadius: 4)) + .contentShape(Rectangle()) } } - } else { - ZStack(alignment: .leading) { - TextEditor(text: $conversationViewModel.messageText) - .multilineTextAlignment(.leading) - .frame(maxHeight: 160) - .fixedSize(horizontal: false, vertical: true) + .frame( + width: geometry.size.width > 0 && CGFloat(102 * conversationViewModel.mediasToSend.count) > geometry.size.width - 20 + ? 102 * floor(CGFloat(geometry.size.width - 20) / 102) + : CGFloat(102 * conversationViewModel.mediasToSend.count) + ) + } + } + .frame(maxWidth: .infinity) + .padding(.all, conversationViewModel.mediasToSend.isEmpty ? 0 : 10) + .background(Color.gray100) + + if !mediasIsLoading { + HStack { + Spacer() + + Button(action: { + withAnimation { + conversationViewModel.mediasToSend.removeAll() + } + }, label: { + Image("x") + .resizable() + .frame(width: 30, height: 30, alignment: .leading) + .padding(.all, 10) + }) + } + } + } + .transition(.move(edge: .bottom)) + } + + HStack(spacing: 0) { + if !voiceRecordingInProgress { + Button { + } label: { + Image("smiley") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 28, height: 28, alignment: .leading) + .padding(.all, 6) + .padding(.top, 4) + } + .padding(.horizontal, isMessageTextFocused ? 0 : 2) + + Button { + self.isShowPhotoLibrary = true + self.mediasIsLoading = true + } label: { + Image("paperclip") + .renderingMode(.template) + .resizable() + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading) + .padding(.all, isMessageTextFocused ? 0 : 6) + .padding(.top, 4) + .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) + } + .padding(.horizontal, isMessageTextFocused ? 0 : 2) + + Button { + self.isShowCamera = true + } label: { + Image("camera") + .renderingMode(.template) + .resizable() + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading) + .padding(.all, isMessageTextFocused ? 0 : 6) + .padding(.top, 4) + .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) + } + .padding(.horizontal, isMessageTextFocused ? 0 : 2) + + HStack { + if #available(iOS 16.0, *) { + TextField("Say something...", text: $conversationViewModel.messageText, axis: .vertical) .default_text_style(styleSize: 15) .focused($isMessageTextFocused) + .padding(.vertical, 5) .onChange(of: conversationViewModel.messageText) { text in if !text.isEmpty && !CoreContext.shared.enteredForeground { conversationViewModel.compose() } } - - if conversationViewModel.messageText.isEmpty { - Text("Say something...") - .padding(.leading, 4) - .lineLimit(1) - .opacity(conversationViewModel.messageText.isEmpty ? 1 : 0) - .foregroundStyle(Color.gray300) + } else { + ZStack(alignment: .leading) { + TextEditor(text: $conversationViewModel.messageText) + .multilineTextAlignment(.leading) + .frame(maxHeight: 160) + .fixedSize(horizontal: false, vertical: true) .default_text_style(styleSize: 15) + .focused($isMessageTextFocused) + .onChange(of: conversationViewModel.messageText) { text in + if !text.isEmpty && !CoreContext.shared.enteredForeground { + conversationViewModel.compose() + } + } + + if conversationViewModel.messageText.isEmpty { + Text("Say something...") + .padding(.leading, 4) + .lineLimit(1) + .opacity(conversationViewModel.messageText.isEmpty ? 1 : 0) + .foregroundStyle(Color.gray300) + .default_text_style(styleSize: 15) + } + } + .onTapGesture { + isMessageTextFocused = true } } - .onTapGesture { - isMessageTextFocused = true - } - } - - if conversationViewModel.messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty { - Button { - voiceRecordingInProgress = true - } label: { - Image("microphone") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 28, height: 28, alignment: .leading) - .padding(.all, 6) - .padding(.top, 4) - } - } else { - Button { - if conversationViewModel.displayedConversationHistorySize > 0 { - NotificationCenter.default.post(name: .onScrollToBottom, object: nil) + + if conversationViewModel.messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty { + Button { + voiceRecordingInProgress = true + } label: { + Image("microphone") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 28, height: 28, alignment: .leading) + .padding(.all, 6) + .padding(.top, 4) } - conversationViewModel.sendMessage() - } label: { - Image("paper-plane-tilt") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 28, height: 28, alignment: .leading) - .padding(.all, 6) - .padding(.top, 4) - .rotationEffect(.degrees(45)) + } else { + Button { + if conversationViewModel.displayedConversationHistorySize > 0 { + NotificationCenter.default.post(name: .onScrollToBottom, object: nil) + } + conversationViewModel.sendMessage() + } label: { + Image("paper-plane-tilt") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 28, height: 28, alignment: .leading) + .padding(.all, 6) + .padding(.top, 4) + .rotationEffect(.degrees(45)) + } + .padding(.trailing, 4) } - .padding(.trailing, 4) } + .padding(.leading, 15) + .padding(.trailing, 5) + .padding(.vertical, 6) + .frame(maxWidth: .infinity, minHeight: 55) + .background(.white) + .cornerRadius(30) + .overlay( + RoundedRectangle(cornerRadius: 30) + .inset(by: 0.5) + .stroke(Color.gray200, lineWidth: 1.5) + ) + .padding(.horizontal, 4) + } else { + VoiceRecorderPlayer(conversationViewModel: conversationViewModel, voiceRecordingInProgress: $voiceRecordingInProgress) + .frame(maxHeight: 60) } - .padding(.leading, 15) - .padding(.trailing, 5) - .padding(.vertical, 6) - .frame(maxWidth: .infinity, minHeight: 55) - .background(.white) - .cornerRadius(30) - .overlay( - RoundedRectangle(cornerRadius: 30) - .inset(by: 0.5) - .stroke(Color.gray200, lineWidth: 1.5) - ) - .padding(.horizontal, 4) - } else { - VoiceRecorderPlayer(conversationViewModel: conversationViewModel, voiceRecordingInProgress: $voiceRecordingInProgress) - .frame(maxHeight: 60) } + .frame(maxWidth: .infinity, minHeight: 60) + .padding(.top, 12) + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? (isMessageTextFocused ? 12 : 0) : 12) + .padding(.horizontal, 10) + .background(Color.gray100) } - .frame(maxWidth: .infinity, minHeight: 60) - .padding(.top, 12) - .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? (isMessageTextFocused ? 12 : 0) : 12) - .padding(.horizontal, 10) - .background(Color.gray100) } } .blur(radius: conversationViewModel.selectedMessage != nil ? 8 : 0) @@ -957,6 +973,19 @@ struct ConversationFragment: View { .transition(.move(edge: .trailing)) } + if isShowInfoConversationFragment { + ConversationInfoFragment( + conversationViewModel: conversationViewModel, + conversationsListViewModel: conversationsListViewModel, + isMuted: $isMuted, + isShowEphemeralFragment: $isShowEphemeralFragment, + isShowStartCallGroupPopup: $isShowStartCallGroupPopup, + isShowInfoConversationFragment: $isShowInfoConversationFragment + ) + .zIndex(5) + .transition(.move(edge: .trailing)) + } + if isShowEphemeralFragment { EphemeralFragment( conversationViewModel: conversationViewModel, diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift new file mode 100644 index 000000000..28b092c53 --- /dev/null +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of Linphone + * + * 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 ConversationInfoFragment: View { + @State private var orientation = UIDevice.current.orientation + + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + + @ObservedObject var conversationViewModel: ConversationViewModel + @ObservedObject var conversationsListViewModel: ConversationsListViewModel + + @Binding var isMuted: Bool + @Binding var isShowEphemeralFragment: Bool + @Binding var isShowStartCallGroupPopup: Bool + @Binding var isShowInfoConversationFragment: Bool + + var body: some View { + NavigationView { + GeometryReader { geometry in + if conversationViewModel.displayedConversation != nil { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + 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 { + withAnimation { + isShowInfoConversationFragment = false + } + } + + Spacer() + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { + VStack(spacing: 0) { + VStack(spacing: 0) { + if #unavailable(iOS 16.0) { + Rectangle() + .foregroundColor(Color.gray100) + .frame(height: 7) + } + + VStack(spacing: 0) { + if conversationViewModel.displayedConversation != nil && !conversationViewModel.displayedConversation!.isGroup { + + Avatar(contactAvatarModel: conversationViewModel.displayedConversation!.avatarModel, avatarSize: 100) + .padding(.top, 4) + + Text(conversationViewModel.displayedConversation!.avatarModel.name) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text(conversationViewModel.displayedConversation!.avatarModel.address) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + + if !conversationViewModel.displayedConversation!.avatarModel.lastPresenceInfo.isEmpty { + Text(conversationViewModel.displayedConversation!.avatarModel.lastPresenceInfo) + .foregroundStyle(conversationViewModel.displayedConversation!.avatarModel.lastPresenceInfo == "Online" + ? Color.greenSuccess500 + : Color.orangeWarning600) + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + .padding(.top, 5) + } else { + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } + } else { + Avatar(contactAvatarModel: conversationViewModel.displayedConversation!.avatarModel, avatarSize: 100) + .padding(.top, 4) + + Text(conversationViewModel.displayedConversation!.avatarModel.name) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + } + } + .frame(minHeight: 150) + .frame(maxWidth: .infinity) + .padding(.top, 10) + .padding(.bottom, 2) + .background(Color.gray100) + + if !conversationViewModel.displayedConversation!.isReadOnly { + HStack { + Spacer() + + Button(action: { + conversationViewModel.displayedConversation!.toggleMute() + isMuted = !isMuted + }, label: { + VStack { + HStack(alignment: .center) { + Image(isMuted ? "bell-simple" : "bell-simple-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text(isMuted ? "conversation_action_unmute" : "conversation_action_mute") + .default_text_style(styleSize: 14) + .frame(minWidth: 80) + .lineLimit(1) + } + }) + .frame(width: geometry.size.width / 4) + + Spacer() + + Button(action: { + if conversationViewModel.displayedConversation!.isGroup { + isShowStartCallGroupPopup.toggle() + } else { + conversationViewModel.displayedConversation!.call() + } + }, label: { + VStack { + HStack(alignment: .center) { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("conversation_action_call") + .default_text_style(styleSize: 14) + .frame(minWidth: 80) + .lineLimit(1) + } + }) + .frame(width: geometry.size.width / 4) + + Spacer() + + Button(action: { + // TODO Create Meeting + }, label: { + VStack { + HStack(alignment: .center) { + Image("video-conference") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("meeting_schedule_meeting_label") + .default_text_style(styleSize: 14) + .frame(minWidth: 80) + .lineLimit(1) + } + }) + .frame(width: geometry.size.width / 4) + + Spacer() + } + .padding(.top, 20) + .padding(.bottom, 10) + .frame(maxWidth: .infinity) + .background(Color.gray100) + } + + Text("contact_details_actions_title") + .default_text_style_800(styleSize: 18) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 20) + .padding(.top, 20) + + VStack(spacing: 0) { + if !conversationViewModel.displayedConversation!.isReadOnly { + if !conversationViewModel.displayedConversation!.isGroup { + Button( + action: { + }, + label: { + HStack { + Image("address-book") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + + Text("conversation_info_menu_go_to_contact") + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + + } + } + ) + .frame(height: 60) + + Divider() + } + + Button( + action: { + withAnimation { + isShowEphemeralFragment = true + } + }, + label: { + HStack { + Image("clock-countdown") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + + Text("conversation_action_configure_ephemeral_messages") + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + + } + } + ) + .frame(height: 60) + + Divider() + + if conversationViewModel.displayedConversation!.isGroup { + Button( + action: { + conversationViewModel.displayedConversation!.leave() + conversationViewModel.displayedConversation!.isReadOnly = true + isShowInfoConversationFragment = false + }, + label: { + HStack { + Image("sign-out") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + + Text("conversation_action_leave_group") + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + + } + } + ) + .frame(height: 60) + + Divider() + } + } + + Button( + action: { + conversationViewModel.displayedConversation!.deleteChatRoom() + conversationsListViewModel.computeChatRoomsList(filter: "") + conversationViewModel.displayedConversation = nil + }, + label: { + HStack { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 25, height: 25) + + Text("conversation_info_delete_history_action") + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + + } + } + ) + .frame(height: 60) + } + .padding(.horizontal, 20) + .padding(.vertical, 4) + .background(.white) + .cornerRadius(15) + .padding(.all) + } + .frame(maxWidth: sharedMainViewModel.maxWidth) + } + .frame(maxWidth: .infinity) + .padding(.top, 2) + } + .background(Color.gray100) + } + .background(.white) + .navigationBarHidden(true) + .onRotate { newOrientation in + orientation = newOrientation + } + } + } + } + .navigationViewStyle(.stack) + } +} + +#Preview { + ConversationInfoFragment( + conversationViewModel: ConversationViewModel(), + conversationsListViewModel: ConversationsListViewModel(), + isMuted: .constant(false), + isShowEphemeralFragment: .constant(false), + isShowStartCallGroupPopup: .constant(false), + isShowInfoConversationFragment: .constant(true) + ) +} diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsFragment.swift index b91bf86e0..4b321f3c5 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationsFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsFragment.swift @@ -39,7 +39,11 @@ struct ConversationsFragment: View { conversationsListViewModel: conversationsListViewModel, showingSheet: $showingSheet ) - .presentationDetents([.fraction(0.4)]) + .presentationDetents( + conversationsListViewModel.selectedConversation != nil && !conversationsListViewModel.selectedConversation!.isReadOnly + ? [.fraction(0.4)] + : [.fraction(0.1)] + ) } } else { ConversationsListFragment(conversationViewModel: conversationViewModel, diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsListBottomSheet.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsListBottomSheet.swift index a7a773704..1bf403391 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationsListBottomSheet.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsListBottomSheet.swift @@ -54,88 +54,11 @@ struct ConversationsListBottomSheet: View { Spacer() - Button { - if conversationsListViewModel.selectedConversation != nil { - conversationsListViewModel.markAsReadSelectedConversation() - conversationsListViewModel.updateUnreadMessagesCount() - } - - if #available(iOS 16.0, *) { - if idiom != .pad { - showingSheet.toggle() - } else { - showingSheet.toggle() - dismiss() - } - } else { - showingSheet.toggle() - dismiss() - } - } label: { - HStack { - Image("envelope-simple") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - Text("Marquer comme non lu") - .default_text_style(styleSize: 16) - Spacer() - } - .frame(maxHeight: .infinity) - } - .padding(.horizontal, 30) - .background(Color.gray100) - - VStack { - Divider() - } - .frame(maxWidth: .infinity) - - Button { - if conversationsListViewModel.selectedConversation != nil { - conversationsListViewModel.selectedConversation!.toggleMute() - } - - if #available(iOS 16.0, *) { - if idiom != .pad { - showingSheet.toggle() - } else { - showingSheet.toggle() - dismiss() - } - } else { - showingSheet.toggle() - dismiss() - } - } label: { - HStack { - Image(conversationsListViewModel.selectedConversation!.isMuted ? "bell" : "bell-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - Text(conversationsListViewModel.selectedConversation!.isMuted ? "Réactiver les notifications" : "Mettre en sourdine") - .default_text_style(styleSize: 16) - Spacer() - } - .frame(maxHeight: .infinity) - } - .padding(.horizontal, 30) - .background(Color.gray100) - - VStack { - Divider() - } - .frame(maxWidth: .infinity) - - if conversationsListViewModel.selectedConversation != nil - && !conversationsListViewModel.selectedConversation!.isGroup { + if conversationsListViewModel.selectedConversation != nil && !conversationsListViewModel.selectedConversation!.isReadOnly { Button { - if !conversationsListViewModel.selectedConversation!.isGroup { - conversationsListViewModel.selectedConversation!.call() + if conversationsListViewModel.selectedConversation != nil { + conversationsListViewModel.markAsReadSelectedConversation() + conversationsListViewModel.updateUnreadMessagesCount() } if #available(iOS 16.0, *) { @@ -149,16 +72,15 @@ struct ConversationsListBottomSheet: View { showingSheet.toggle() dismiss() } - } label: { HStack { - Image("phone") + Image("envelope-simple") .renderingMode(.template) .resizable() .foregroundStyle(Color.grayMain2c500) .frame(width: 25, height: 25, alignment: .leading) .padding(.all, 10) - Text("Appel") + Text("Marquer comme non lu") .default_text_style(styleSize: 16) Spacer() } @@ -171,9 +93,89 @@ struct ConversationsListBottomSheet: View { Divider() } .frame(maxWidth: .infinity) + + Button { + if conversationsListViewModel.selectedConversation != nil { + conversationsListViewModel.selectedConversation!.toggleMute() + } + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image(conversationsListViewModel.selectedConversation!.isMuted ? "bell" : "bell-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + Text(conversationsListViewModel.selectedConversation!.isMuted ? "Réactiver les notifications" : "Mettre en sourdine") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + if conversationsListViewModel.selectedConversation != nil + && !conversationsListViewModel.selectedConversation!.isGroup { + Button { + if !conversationsListViewModel.selectedConversation!.isGroup { + conversationsListViewModel.selectedConversation!.call() + } + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + + } label: { + HStack { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + Text("Appel") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + } } - Button { + Button { conversationsListViewModel.selectedConversation!.deleteChatRoom() conversationsListViewModel.computeChatRoomsList(filter: "") @@ -206,43 +208,46 @@ struct ConversationsListBottomSheet: View { .padding(.horizontal, 30) .background(Color.gray100) - VStack { - Divider() - } - .frame(maxWidth: .infinity) - - Button { - if conversationsListViewModel.selectedConversation != nil { - conversationsListViewModel.selectedConversation!.leave() + if conversationsListViewModel.selectedConversation != nil && !conversationsListViewModel.selectedConversation!.isReadOnly { + VStack { + Divider() } + .frame(maxWidth: .infinity) - if #available(iOS 16.0, *) { - if idiom != .pad { - showingSheet.toggle() + Button { + if conversationsListViewModel.selectedConversation != nil { + conversationsListViewModel.selectedConversation!.leave() + conversationsListViewModel.selectedConversation!.isReadOnly = true + } + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } } else { showingSheet.toggle() dismiss() } - } else { - showingSheet.toggle() - dismiss() + } label: { + HStack { + Image("sign-out") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + Text("Quitter la conversation") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) } - } label: { - HStack { - Image("sign-out") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - Text("Quitter la conversation") - .default_text_style(styleSize: 16) - Spacer() - } - .frame(maxHeight: .infinity) + .padding(.horizontal, 30) + .background(Color.gray100) } - .padding(.horizontal, 30) - .background(Color.gray100) } .background(Color.gray100) .frame(maxWidth: .infinity) diff --git a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift index 587c433b8..1f3c2ffd3 100644 --- a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift +++ b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift @@ -36,7 +36,7 @@ class ConversationModel: ObservableObject, Identifiable { let localSipUri: String let remoteSipUri: String let isGroup: Bool - let isReadOnly: Bool + @Published var isReadOnly: Bool @Published var subject: String @Published var participantsAddress: [String] = [] @Published var isComposing: Bool