diff --git a/Linphone/GeneratedGit.swift b/Linphone/GeneratedGit.swift index ec24c7abb..8ffb5a75a 100644 --- a/Linphone/GeneratedGit.swift +++ b/Linphone/GeneratedGit.swift @@ -2,6 +2,6 @@ import Foundation public enum AppGitInfo { public static let branch = "master" - public static let commit = "990d2f36a" + public static let commit = "6575a4b0f" public static let tag = "6.1.0-alpha" } diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 6c9dae79b..7c4bc1f26 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -251,8 +251,11 @@ "conversation_info_participant_is_admin_label" = "Admin"; "conversation_info_participants_list_title" = "Group members (%d)"; "conversation_invalid_participant_due_to_security_mode_toast" = "Can't create conversation with a participant not on the same domain due to security restrictions!"; -"conversation_menu_configure_ephemeral_messages" = "Ephemeral messages"; +"conversation_menu_search_in_messages" = "Search"; "conversation_menu_go_to_info" = "Conversation info"; +"conversation_menu_configure_ephemeral_messages" = "Ephemeral messages"; +"conversation_menu_media_files" = "Media"; +"conversation_menu_documents_files" = "Documents"; "conversation_message_forward_cancelled_toast" = "Message forward was cancelled"; "conversation_message_forwarded_toast" = "Message was forwarded"; "conversation_message_meeting_cancelled_label" = "Meeting has been cancelled!"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 19e9a0803..35e2c4a7b 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -251,8 +251,11 @@ "conversation_info_participant_is_admin_label" = "Administrateur"; "conversation_info_participants_list_title" = "Participants (%d)"; "conversation_invalid_participant_due_to_security_mode_toast" = "Pour des raisons de sécurité, la création d'une conversation avec un participant d'un domaine tiers est désactivé."; -"conversation_menu_configure_ephemeral_messages" = "Messages éphémères"; +"conversation_menu_search_in_messages" = "Chercher"; "conversation_menu_go_to_info" = "Informations"; +"conversation_menu_configure_ephemeral_messages" = "Messages éphémères"; +"conversation_menu_media_files" = "Médias"; +"conversation_menu_documents_files" = "Documents"; "conversation_message_forward_cancelled_toast" = "Transfert annulé"; "conversation_message_forwarded_toast" = "Message transféré"; "conversation_message_meeting_cancelled_label" = "La réunion a été annulée"; diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 9954be02b..b9bf8c382 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -981,7 +981,7 @@ struct DynamicLinkText: View { if let encoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encoded), - ["http", "https"].contains(url.scheme) + ["http", "https", "sip", "sips"].contains(url.scheme) { var link = AttributedString(word) link.link = url @@ -999,7 +999,7 @@ struct DynamicLinkText: View { var mention = AttributedString("@" + participant.name) mention.link = mentionURL mention.foregroundColor = Color.orangeMain500 - mention.font = .system(size: 14, weight: .semibold) + mention.font = .system(size: 14) result.append(mention) return } diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index bfc7467e5..482b7a0a3 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -41,6 +41,7 @@ struct ConversationFragment: View { @State var isMenuOpen = false @State private var isMuted: Bool = false + @FocusState var isSearchTextFocused: Bool @FocusState var isMessageTextFocused: Bool @State var offset: CGPoint = .zero @@ -81,11 +82,13 @@ struct ConversationFragment: View { @Binding var isShowConversationInfoPopup: Bool @Binding var conversationInfoPopupText: String + @State var searchText: String = "" @State var messageText: String = "" @State private var chosen: String? @State private var showPicker = false @State private var isSheetVisible = false + @State private var isSearchVisible = false @State private var isImdnOrReactionsSheetVisible = false @@ -292,122 +295,105 @@ struct ConversationFragment: View { .edgesIgnoringSafeArea(.top) .frame(height: 0) - HStack { - if (!(orientation == .landscapeLeft || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height)) || isShowConversationFragment { - Image("caret-left") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - .padding(.top, 4) - .padding(.leading, -10) - .onTapGesture { - withAnimation { - if isShowConversationFragment { - isShowConversationFragment = false - } - SharedMainViewModel.shared.displayedConversation = nil - } - } - } - - Avatar(contactAvatarModel: SharedMainViewModel.shared.displayedConversation?.avatarModel ?? cachedConversation!.avatarModel, avatarSize: 50) - .padding(.top, 4) - - VStack(spacing: 1) { - Text(SharedMainViewModel.shared.displayedConversation?.subject ?? cachedConversation!.subject) - .default_text_style(styleSize: 16) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 4) - .lineLimit(1) - - if isMuted || conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") { - HStack { - if isMuted { - Image("bell-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 16, height: 16, alignment: .trailing) - } - - if conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") { - Image("clock-countdown") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 16, height: 16, alignment: .trailing) - - Text(conversationViewModel.ephemeralTime) - .default_text_style(styleSize: 12) - .padding(.leading, -2) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) - } - - Spacer() - } - } - } - .background(.white) - .onTapGesture { - withAnimation { - isShowInfoConversationFragment = true - } - } - .padding(.vertical, 10) - - Spacer() - - if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) { - Button { - if SharedMainViewModel.shared.displayedConversation!.isGroup { - isShowStartCallGroupPopup.toggle() - } else { - SharedMainViewModel.shared.displayedConversation!.call() - } - } label: { - Image("phone") + if !isSearchVisible { + HStack { + if (!(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height)) || isShowConversationFragment { + Image("caret-left") .renderingMode(.template) .resizable() - .foregroundStyle(Color.grayMain2c500) + .foregroundStyle(Color.orangeMain500) .frame(width: 25, height: 25, alignment: .leading) .padding(.all, 10) .padding(.top, 4) + .padding(.leading, -10) + .onTapGesture { + withAnimation { + if isShowConversationFragment { + isShowConversationFragment = false + } + SharedMainViewModel.shared.displayedConversation = nil + } + } } - } - - Menu { - Button { - isMenuOpen = false + + Avatar(contactAvatarModel: SharedMainViewModel.shared.displayedConversation?.avatarModel ?? cachedConversation!.avatarModel, avatarSize: 50) + .padding(.top, 4) + + VStack(spacing: 1) { + Text(SharedMainViewModel.shared.displayedConversation?.subject ?? cachedConversation!.subject) + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + + if isMuted || conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") { + HStack { + if isMuted { + Image("bell-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 16, height: 16, alignment: .trailing) + } + + if conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") { + Image("clock-countdown") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 16, height: 16, alignment: .trailing) + + Text(conversationViewModel.ephemeralTime) + .default_text_style(styleSize: 12) + .padding(.leading, -2) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + + Spacer() + } + } + } + .background(.white) + .onTapGesture { withAnimation { isShowInfoConversationFragment = true } - } label: { - HStack { - Text("conversation_menu_go_to_info") - Spacer() - Image("info") + } + .padding(.vertical, 10) + + Spacer() + + if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) { + Button { + if SharedMainViewModel.shared.displayedConversation!.isGroup { + isShowStartCallGroupPopup.toggle() + } else { + SharedMainViewModel.shared.displayedConversation!.call() + } + } label: { + Image("phone") .renderingMode(.template) .resizable() .foregroundStyle(Color.grayMain2c500) .frame(width: 25, height: 25, alignment: .leading) .padding(.all, 10) + .padding(.top, 4) } } - if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) { + Menu { Button { isMenuOpen = false - SharedMainViewModel.shared.displayedConversation!.toggleMute() - isMuted = !isMuted + withAnimation { + isShowInfoConversationFragment = true + } } label: { HStack { - Text(isMuted ? "conversation_action_unmute" : "conversation_action_mute") + Text("conversation_menu_go_to_info") Spacer() - Image(isMuted ? "bell-simple" : "bell-simple-slash") + Image("info") .renderingMode(.template) .resizable() .foregroundStyle(Color.grayMain2c500) @@ -419,13 +405,14 @@ struct ConversationFragment: View { Button { isMenuOpen = false withAnimation { - isShowEphemeralFragment = true + isSearchVisible = true } + isSearchTextFocused = true } label: { HStack { - Text("conversation_menu_configure_ephemeral_messages") + Text("conversation_menu_search_in_messages") Spacer() - Image("clock-countdown") + Image("magnifying-glass") .renderingMode(.template) .resizable() .foregroundStyle(Color.grayMain2c500) @@ -433,29 +420,123 @@ struct ConversationFragment: View { .padding(.all, 10) } } + + if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) { + Button { + isMenuOpen = false + SharedMainViewModel.shared.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) + } + } + } + } label: { + Image("dots-three-vertical") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) + .onChange(of: isMuted) { _ in } + .onAppear { + isMuted = SharedMainViewModel.shared.displayedConversation!.isMuted + } } - } label: { - Image("dots-three-vertical") + .onTapGesture { + isMenuOpen = true + } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + } else { + HStack { + Image("caret-left") .renderingMode(.template) .resizable() .foregroundStyle(Color.grayMain2c500) .frame(width: 25, height: 25, alignment: .leading) .padding(.all, 10) .padding(.top, 4) - .onChange(of: isMuted) { _ in } - .onAppear { - isMuted = SharedMainViewModel.shared.displayedConversation!.isMuted + .padding(.leading, -10) + .onTapGesture { + searchText = "" + conversationViewModel.latestMatch = nil + withAnimation { + isSearchVisible = false + } } + + TextField("conversation_menu_search_in_messages", text: $searchText) + .default_text_style(styleSize: 15) + .focused($isSearchTextFocused) + .padding(.vertical, 5) + .submitLabel(.search) + .onSubmit { + conversationViewModel.searchChatMessage(direction: .Up, textToSearch: searchText) + } + + Button { + // TODO + } label: { + Image("caret-up") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) + } + + Button { + // TODO + } label: { + Image("caret-down") + .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) } - .frame(maxWidth: .infinity) - .frame(height: 50) - .padding(.horizontal) - .padding(.bottom, 4) - .background(.white) if #available(iOS 16.0, *) { ZStack(alignment: .bottomTrailing) { @@ -593,7 +674,7 @@ struct ConversationFragment: View { .transition(.move(edge: .bottom)) } - if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) { + if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) && !isSearchVisible { if conversationViewModel.messageToReply != nil { ZStack(alignment: .top) { HStack { diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 84611e0c5..127820efe 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -106,6 +106,10 @@ class ConversationViewModel: ObservableObject { @Published var isSwiping = false + @Published var searchInProgress = false + + var latestMatch: EventLogMessage? + struct SheetCategory: Identifiable { let id = UUID() let name: String @@ -120,10 +124,10 @@ class ConversationViewModel: ObservableObject { } init() { - if let chatroom = self.sharedMainViewModel.displayedConversation?.chatRoom { - self.addConversationDelegate(chatRoom: chatroom) - self.getMessages() - } + if let chatroom = self.sharedMainViewModel.displayedConversation?.chatRoom { + self.addConversationDelegate(chatRoom: chatroom) + self.getMessages() + } } func addConversationDelegate(chatRoom: ChatRoom) { @@ -300,7 +304,7 @@ class ConversationViewModel: ObservableObject { if let displayedConversation = self.sharedMainViewModel.displayedConversation { displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom) } - + DispatchQueue.main.async { if indexMessage != nil { self.conversationMessagesSection[0].rows[indexMessage!].message.text = "" @@ -444,7 +448,7 @@ class ConversationViewModel: ObservableObject { Log.error("[ConversationViewModel] Invalid contentIndex") return } - + self.conversationMessagesSection[0].rows[indexMessage].message.attachments[contentIndex] = newAttachment let attachmentIndex = self.getAttachmentIndex(attachment: newAttachment) @@ -678,7 +682,7 @@ class ConversationViewModel: ObservableObject { self.getUnreadMessagesCount() self.getParticipantConversationModel() self.computeComposingLabel() - self.getEphemeralTime() + self.getEphemeralTime() if self.sharedMainViewModel.displayedConversation != nil { let historyEvents = self.sharedMainViewModel.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30) @@ -1713,13 +1717,13 @@ class ConversationViewModel: ObservableObject { if let eventLogMessage = conversationMessagesTmp.last { DispatchQueue.main.async { - Log.info("[ConversationViewModel] Send first message") - if self.conversationMessagesSection.isEmpty && self.sharedMainViewModel.displayedConversation != nil { - self.conversationMessagesSection.append(MessagesSection(date: Date(), chatRoomID: self.sharedMainViewModel.displayedConversation!.id, rows: conversationMessagesTmp)) - } else { - self.conversationMessagesSection[0].rows.append(eventLogMessage) - } - } + Log.info("[ConversationViewModel] Send first message") + if self.conversationMessagesSection.isEmpty && self.sharedMainViewModel.displayedConversation != nil { + self.conversationMessagesSection.append(MessagesSection(date: Date(), chatRoomID: self.sharedMainViewModel.displayedConversation!.id, rows: conversationMessagesTmp)) + } else { + self.conversationMessagesSection[0].rows.append(eventLogMessage) + } + } } getHistorySize() @@ -1762,18 +1766,18 @@ class ConversationViewModel: ObservableObject { conversationMessagesSection = [] } - func replyToMessage(index: Int, isMessageTextFocused: Binding) { + func replyToMessage(index: Int, isMessageTextFocused: Binding) { if self.messageToEdit != nil { self.messageToEdit = nil } coreContext.doOnCoreQueue { _ in let messageToReplyTmp = self.conversationMessagesSection[0].rows[index] - DispatchQueue.main.async { - withAnimation(.linear(duration: 0.15)) { - self.messageToReply = messageToReplyTmp - } - isMessageTextFocused.wrappedValue = true - } + DispatchQueue.main.async { + withAnimation(.linear(duration: 0.15)) { + self.messageToReply = messageToReplyTmp + } + isMessageTextFocused.wrappedValue = true + } } } @@ -1889,7 +1893,7 @@ class ConversationViewModel: ObservableObject { } else { if content.type != "video" { let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/") - let path = URL(string: self.getNewFilePath(name: filePathSep[1])) + let path = URL(string: self.getNewFilePath(name: filePathSep[1])) var typeTmp: AttachmentType = .other switch content.type { @@ -1928,7 +1932,7 @@ class ConversationViewModel: ObservableObject { } } else if content.type == "video" { let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/") - let path = URL(string: self.getNewFilePath(name: filePathSep[1])) + let path = URL(string: self.getNewFilePath(name: filePathSep[1])) let pathThumbnail = URL(string: self.generateThumbnail(name: filePathSep[1])) if path != nil && pathThumbnail != nil { @@ -2964,6 +2968,66 @@ class ConversationViewModel: ObservableObject { } } } + + func searchChatMessage(direction: SearchDirection, textToSearch: String) { + if let displayedConversation = self.sharedMainViewModel.displayedConversation { + searchInProgress = true + + if let match = displayedConversation.chatRoom.searchChatMessageByText(text: textToSearch, from: latestMatch?.eventModel.eventLog ?? nil, direction: direction) { + + Log.info("\(ConversationViewModel.TAG) Found result \(match.chatMessage?.messageId ?? "No message id") while looking up for message with text \(textToSearch) in direction \(direction) starting from message \(latestMatch?.eventModel.eventLog.chatMessage?.messageId ?? "No message id")" + ) + + if let sectionIndex = conversationMessagesSection.firstIndex(where: { + $0.chatRoomID == displayedConversation.id + }), + let rowIndex = conversationMessagesSection[sectionIndex].rows.firstIndex(where: { + $0.eventModel.eventLogId == match.chatMessage?.messageId + }) { + latestMatch = conversationMessagesSection[sectionIndex].rows[rowIndex] + + Log.info("\(ConversationViewModel.TAG) Found result is already in history, no need to load more history") + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onScrollToIndex"), object: nil, userInfo: ["index": rowIndex, "animated": true]) + } + print("searchChatMessageAAA 00") + + searchInProgress = false + } else { + latestMatch = nil + + Log.info("\(ConversationViewModel.TAG) Found result isn't in currently loaded history, loading missing events") + //loadMessagesUpTo(match) + print("searchChatMessageAAA 11") + } + + //canSearchDown = true + } else { + Log.info("\(ConversationViewModel.TAG) No match found while looking up for message with text \(textToSearch) in direction \(direction) starting from message \(latestMatch?.eventModel.eventLog.chatMessage?.messageId ?? "No message id")" + ) + searchInProgress = false + if latestMatch == nil { + print("searchChatMessageAAA 22") + // R.string.conversation_search_no_match_found + } else { + print("searchChatMessageAAA 33") + // Scroll to last matching event anyway, user may have scrolled away + if let sectionIndex = conversationMessagesSection.firstIndex(where: { + $0.chatRoomID == displayedConversation.id + }), let latestMatchTmp = latestMatch, + let rowIndex = conversationMessagesSection[sectionIndex].rows.firstIndex(of: latestMatchTmp) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onScrollToIndex"), object: nil, userInfo: ["index": rowIndex, "animated": true]) + } + print("searchChatMessageAAA 33Bis") + } + // R.string.conversation_search_no_more_match + } + // showRedToast(message, R.drawable.magnifying_glass) + } + } + } } // swiftlint:enable line_length // swiftlint:enable type_body_length