Add message search feature

This commit is contained in:
Benoit Martins 2026-01-08 17:42:03 +01:00
parent 6575a4b0f2
commit d111e03eef
6 changed files with 290 additions and 139 deletions

View file

@ -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"
}

View file

@ -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!";

View file

@ -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";

View file

@ -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
}

View file

@ -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,6 +295,7 @@ struct ConversationFragment: View {
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
if !isSearchVisible {
HStack {
if (!(orientation == .landscapeLeft || orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height)) || isShowConversationFragment {
@ -398,6 +402,25 @@ struct ConversationFragment: View {
}
}
Button {
isMenuOpen = false
withAnimation {
isSearchVisible = true
}
isSearchTextFocused = true
} label: {
HStack {
Text("conversation_menu_search_in_messages")
Spacer()
Image("magnifying-glass")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
}
if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) {
Button {
isMenuOpen = false
@ -456,6 +479,64 @@ struct ConversationFragment: View {
.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)
.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)
}
}
.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 {

View file

@ -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
@ -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