diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 46ca823f7..ef1a0b262 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -23,6 +23,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 */; }; + 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 */; }; D719ABBB2ABC67BF00B41C10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D719ABBA2ABC67BF00B41C10 /* Assets.xcassets */; }; @@ -124,6 +125,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 = ""; }; + 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 = ""; }; D719ABB82ABC67BF00B41C10 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -562,6 +564,7 @@ D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */, D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */, D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */, + D71968912B86369D00DF4459 /* ChatBubbleView.swift */, ); path = Fragments; sourceTree = ""; @@ -796,6 +799,7 @@ D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, D7CEE0352B7A210300FD79B7 /* ConversationsView.swift in Sources */, D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */, + D71968922B86369D00DF4459 /* ChatBubbleView.swift in Sources */, D78290B82ADD3910004AA85C /* ContactsFragment.swift in Sources */, D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */, D7CEE03D2B7A23B200FD79B7 /* ConversationsListFragment.swift in Sources */, diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 92b748694..06435c86a 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -46,6 +46,7 @@ struct LinphoneApp: App { @State private var startCallViewModel: StartCallViewModel? @State private var callViewModel: CallViewModel? @State private var conversationsListViewModel: ConversationsListViewModel? + @State private var conversationViewModel: ConversationViewModel? var body: some Scene { WindowGroup { @@ -67,7 +68,8 @@ struct LinphoneApp: App { && historyListViewModel != nil && startCallViewModel != nil && callViewModel != nil - && conversationsListViewModel != nil{ + && conversationsListViewModel != nil + && conversationViewModel != nil { ContentView( contactViewModel: contactViewModel!, editContactViewModel: editContactViewModel!, @@ -75,7 +77,8 @@ struct LinphoneApp: App { historyListViewModel: historyListViewModel!, startCallViewModel: startCallViewModel!, callViewModel: callViewModel!, - conversationsListViewModel: conversationsListViewModel! + conversationsListViewModel: conversationsListViewModel!, + conversationViewModel: conversationViewModel! ) } else { SplashScreen() @@ -90,6 +93,7 @@ struct LinphoneApp: App { startCallViewModel = StartCallViewModel() callViewModel = CallViewModel() conversationsListViewModel = ConversationsListViewModel() + conversationViewModel = ConversationViewModel() } } } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 3b9a04413..068eac355 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -244,6 +244,9 @@ }, "Contacts" : { + }, + "Content" : { + }, "Continue" : { @@ -346,6 +349,9 @@ }, "Headphones" : { + }, + "Hello, World!" : { + }, "History has been deleted" : { @@ -522,6 +528,9 @@ }, "Say %@ and click on the letters given by your correspondent:" : { + }, + "Say something..." : { + }, "Scan QR code" : { @@ -585,6 +594,9 @@ }, "This contact will be deleted definitively." : { + }, + "Title" : { + }, "TLS" : { diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 938aa7b42..696570e98 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -41,6 +41,7 @@ struct ContentView: View { @ObservedObject var startCallViewModel: StartCallViewModel @ObservedObject var callViewModel: CallViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel + @ObservedObject var conversationViewModel: ConversationViewModel @State var index = 0 @State private var orientation = UIDevice.current.orientation @@ -89,6 +90,7 @@ struct ContentView: View { Button(action: { self.index = 0 historyViewModel.displayedCall = nil + conversationsListViewModel.displayedConversation = nil }, label: { VStack { Image("address-book") @@ -132,6 +134,7 @@ struct ContentView: View { Button(action: { self.index = 1 contactViewModel.indexDisplayedFriend = nil + conversationsListViewModel.displayedConversation = nil if historyListViewModel.missedCallsCount > 0 { historyListViewModel.resetMissedCallsCount() } @@ -480,6 +483,7 @@ struct ContentView: View { Button(action: { self.index = 0 historyViewModel.displayedCall = nil + conversationsListViewModel.displayedConversation = nil }, label: { VStack { Image("address-book") @@ -525,6 +529,7 @@ struct ContentView: View { Button(action: { self.index = 1 contactViewModel.indexDisplayedFriend = nil + conversationsListViewModel.displayedConversation = nil if historyListViewModel.missedCallsCount > 0 { historyListViewModel.resetMissedCallsCount() } @@ -608,7 +613,7 @@ struct ContentView: View { } } - if contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil { + if contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil || conversationsListViewModel.displayedConversation != nil { HStack(spacing: 0) { Spacer() .frame(maxWidth: @@ -657,7 +662,7 @@ struct ContentView: View { .ignoresSafeArea(.keyboard) } } else if self.index == 2 { - ConversationsView(conversationsListViewModel: conversationsListViewModel) + ConversationFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel) .frame(maxWidth: .infinity) .background(Color.gray100) .ignoresSafeArea(.keyboard) @@ -873,7 +878,7 @@ struct ContentView: View { } } .onRotate { newOrientation in - if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil) && searchIsActive { + if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil || conversationsListViewModel.displayedConversation != nil) && searchIsActive { self.focusedField = false } else if searchIsActive { self.focusedField = true @@ -916,7 +921,8 @@ struct ContentView: View { historyListViewModel: HistoryListViewModel(), startCallViewModel: StartCallViewModel(), callViewModel: CallViewModel(), - conversationsListViewModel: ConversationsListViewModel() + conversationsListViewModel: ConversationsListViewModel(), + conversationViewModel: ConversationViewModel() ) } // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift new file mode 100644 index 000000000..2f4d7f7fc --- /dev/null +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2023 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 ChatBubbleView: View { + + @ObservedObject var conversationViewModel: ConversationViewModel + + let index: Int + + var body: some View { + Text(conversationViewModel.getMessage(index: index)) + } +} + +#Preview { + ChatBubbleView(conversationViewModel: ConversationViewModel(), index: 0) +} diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 86db6c6e1..02c6e259f 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -1,18 +1,370 @@ -// -// ConversationFragment.swift -// Linphone -// -// Created by Martins BenoƮt on 16/02/2024. -// +/* + * Copyright (c) 2010-2023 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 ConversationFragment: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } + + @State private var orientation = UIDevice.current.orientation + + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var conversationViewModel: ConversationViewModel + @ObservedObject var conversationsListViewModel: ConversationsListViewModel + + @State var isMenuOpen = false + + @FocusState var isMessageTextFocused: Bool + + var body: some View { + NavigationView { + GeometryReader { geometry in + VStack(spacing: 1) { + if conversationViewModel.displayedConversation != nil { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + if !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) + .padding(.leading, -10) + .onTapGesture { + withAnimation { + conversationsListViewModel.displayedConversation = nil + } + } + } + + let addressFriend = + (conversationsListViewModel.displayedConversation!.participants.first != nil && conversationsListViewModel.displayedConversation!.participants.first!.address != nil) + ? contactsManager.getFriendWithAddress(address: conversationsListViewModel.displayedConversation!.participants.first!.address!) + : nil + + let contactAvatarModel = addressFriend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) + && $0.friend!.name == addressFriend!.name + && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() + }) + : ContactAvatarModel(friend: nil, withPresence: false) + + if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationsListViewModel.displayedConversation!) { + Image(uiImage: contactsManager.textToImage( + firstName: conversationsListViewModel.displayedConversation!.subject!, + lastName: conversationsListViewModel.displayedConversation!.subject!.components(separatedBy: " ").count > 1 + ? conversationsListViewModel.displayedConversation!.subject!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(.top, 4) + } else if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { + if contactAvatarModel != nil { + Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 50) + .padding(.top, 4) + } else { + Image("profil-picture-default") + .resizable() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(.top, 4) + } + } else { + if conversationsListViewModel.displayedConversation!.participants.first != nil + && conversationsListViewModel.displayedConversation!.participants.first!.address != nil { + if conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName != nil { + Image(uiImage: contactsManager.textToImage( + firstName: conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName!, + lastName: conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName!.components(separatedBy: " ").count > 1 + ? conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(.top, 4) + + } else { + Image(uiImage: contactsManager.textToImage( + firstName: conversationsListViewModel.displayedConversation!.participants.first!.address!.username ?? "Username Error", + lastName: conversationsListViewModel.displayedConversation!.participants.first!.address!.username!.components(separatedBy: " ").count > 1 + ? conversationsListViewModel.displayedConversation!.participants.first!.address!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(.top, 4) + } + + } else { + Image("profil-picture-default") + .resizable() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(.top, 4) + } + } + + if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationsListViewModel.displayedConversation!) { + Text(conversationsListViewModel.displayedConversation!.subject ?? "No Subject") + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + } else if addressFriend != nil { + Text(addressFriend!.name!) + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + } else { + if conversationsListViewModel.displayedConversation!.participants.first != nil + && conversationsListViewModel.displayedConversation!.participants.first!.address != nil { + Text(conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName != nil + ? conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName! + : conversationsListViewModel.displayedConversation!.participants.first!.address!.username!) + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + } + } + + Spacer() + + Button { + } 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 + } label: { + HStack { + Text("See contact") + Spacer() + Image("user-circle") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + } + + Button { + isMenuOpen = false + } label: { + HStack { + Text("Copy SIP address") + Spacer() + Image("copy") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + } + + Button(role: .destructive) { + isMenuOpen = false + } label: { + HStack { + Text("Delete history") + Spacer() + Image("trash-simple-red") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + } + } label: { + Image("dots-three-vertical") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) + } + .onTapGesture { + isMenuOpen = true + } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + List { + if conversationsListViewModel.displayedConversation != nil { + ForEach(0.. 0 ? (isMessageTextFocused ? 12 : 0) : 12) + .padding(.horizontal, 10) + .background(Color.gray100) + } + } + .background(.white) + .navigationBarHidden(true) + .onRotate { newOrientation in + orientation = newOrientation + } + } + } + .navigationViewStyle(.stack) + } } -#Preview { - ConversationFragment() +extension UIApplication { + func endEditing() { + sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} + +#Preview { + ConversationFragment(conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel()) } diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift index 5958ac9a0..6c5f6a757 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift @@ -226,6 +226,9 @@ struct ConversationsListFragment: View { .listRowSeparator(.hidden) .background(.white) .onTapGesture { + withAnimation { + conversationsListViewModel.displayedConversation = conversationsListViewModel.conversationsList[index] + } } .onLongPressGesture(minimumDuration: 0.2) { conversationsListViewModel.selectedConversation = conversationsListViewModel.conversationsList[index] diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index d71a061b1..f0d5cc7cb 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -22,5 +22,18 @@ import linphonesw class ConversationViewModel: ObservableObject { + @Published var displayedConversation: ChatRoom? + + @Published var messageText: String = "" + init() {} + + func getMessage(index: Int) -> String { + if self.displayedConversation != nil { + return displayedConversation!.getHistoryRangeEvents(begin: index, end: index+1).first?.chatMessage?.utf8Text ?? "" + } + else { + return "" + } + } } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index 5db41c604..094aa5b04 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -31,8 +31,6 @@ class ConversationsListViewModel: ObservableObject { @Published var conversationsList: [ChatRoom] = [] @Published var unreadMessages: Int = 0 - @Published var displayedConversation: ChatRoom? - var selectedConversation: ChatRoom? init() {