imp: default contact list of global search screen

pull/891/head
Ryan ZHAO 1 year ago
parent b443e72092
commit a4286212b4

@ -169,6 +169,7 @@
7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */ = {isa = PBXBuildFile; fileRef = 7BFD1A962747689000FB91B9 /* Session-Turn-Server */; };
942C9CA22B67769000B5153A /* SessionSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942C9CA12B67769000B5153A /* SessionSearchBar.swift */; };
942C9CA42B6868B800B5153A /* GlobalSearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942C9CA32B6868B800B5153A /* GlobalSearchScreen.swift */; };
943C6D762B705B7D004ACE64 /* CompatibleScrollingVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D752B705B7D004ACE64 /* CompatibleScrollingVStack.swift */; };
946B34472B5DF0B7004CB4A3 /* QRCodeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 946B34462B5DF0B7004CB4A3 /* QRCodeScreen.swift */; };
946B34492B5E04BB004CB4A3 /* CustomTopTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 946B34482B5E04BB004CB4A3 /* CustomTopTabBar.swift */; };
946B344B2B5E08F3004CB4A3 /* ScanQRCodeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 946B344A2B5E08F3004CB4A3 /* ScanQRCodeScreen.swift */; };
@ -1312,6 +1313,7 @@
93359C81CF2660040B7CD106 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
942C9CA12B67769000B5153A /* SessionSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionSearchBar.swift; sourceTree = "<group>"; };
942C9CA32B6868B800B5153A /* GlobalSearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreen.swift; sourceTree = "<group>"; };
943C6D752B705B7D004ACE64 /* CompatibleScrollingVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleScrollingVStack.swift; sourceTree = "<group>"; };
946B34462B5DF0B7004CB4A3 /* QRCodeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScreen.swift; sourceTree = "<group>"; };
946B34482B5E04BB004CB4A3 /* CustomTopTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTopTabBar.swift; sourceTree = "<group>"; };
946B344A2B5E08F3004CB4A3 /* ScanQRCodeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeScreen.swift; sourceTree = "<group>"; };
@ -2501,6 +2503,7 @@
7B87EF432A8DA720002A0E8F /* SessionTextField.swift */,
7BF570D22A9C1F9300DB013E /* Toast.swift */,
942C9CA12B67769000B5153A /* SessionSearchBar.swift */,
943C6D752B705B7D004ACE64 /* CompatibleScrollingVStack.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
@ -5683,6 +5686,7 @@
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */,
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */,
943C6D762B705B7D004ACE64 /* CompatibleScrollingVStack.swift in Sources */,
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */,
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */,
FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */,

@ -18,6 +18,11 @@ enum SearchSection: Int, Differentiable {
struct GlobalSearchScreen: View {
fileprivate typealias SectionModel = ArraySection<SearchSection, SessionThreadViewModel>
struct SectionData {
var sectionName: String
var contacts: [SessionThreadViewModel]
}
@EnvironmentObject var host: HostWrapper
@ -29,16 +34,54 @@ struct GlobalSearchScreen: View {
@State private var isLoading = false
fileprivate static var defaultSearchResults: [SectionModel] = {
let result: SessionThreadViewModel? = Storage.shared.read { db -> SessionThreadViewModel? in
let result: [SessionThreadViewModel]? = Storage.shared.read { db -> [SessionThreadViewModel]? in
try SessionThreadViewModel
.noteToSelfOnlyQuery(userPublicKey: getUserHexEncodedPublicKey(db))
.fetchOne(db)
.defaultContactsQuery(userPublicKey: getUserHexEncodedPublicKey(db))
.fetchAll(db)
}
return [ result.map { ArraySection(model: .defaultContacts, elements: [$0]) } ]
return [ result.map { ArraySection(model: .defaultContacts, elements: $0) } ]
.compactMap { $0 }
}()
fileprivate var defaultGroupedContacts: [SectionData] = {
let contacts = Self.defaultSearchResults[0].elements
var groupedContacts: [String: SectionData] = [:]
contacts.forEach { contactViewModel in
guard !contactViewModel.threadIsNoteToSelf else {
groupedContacts[""] = SectionData(
sectionName: "",
contacts: [contactViewModel]
)
return
}
let displayName = NSMutableString(string: contactViewModel.displayName)
CFStringTransform(displayName, nil, kCFStringTransformToLatin, false)
CFStringTransform(displayName, nil, kCFStringTransformStripDiacritics, false)
let initialCharacter: String = (displayName.length > 0 ? displayName.substring(to: 1) : "")
let section: String = initialCharacter.capitalized.isSingleAlphabet ?
initialCharacter.capitalized :
"Unknown"
if groupedContacts[section] == nil {
groupedContacts[section] = SectionData(
sectionName: section,
contacts: []
)
}
groupedContacts[section]?.contacts.append(contactViewModel)
}
return groupedContacts.values.sorted {
if $0.sectionName.count != $1.sectionName.count {
return $0.sectionName.count < $1.sectionName.count
}
return $0.sectionName < $1.sectionName
}
}()
var body: some View {
VStack(alignment: .leading) {
SessionSearchBar(
@ -50,19 +93,21 @@ struct GlobalSearchScreen: View {
}
)
List{
CompatibleScrollingVStack(
alignment: .leading
) {
ForEach(0..<searchResultSet.count, id: \.self) { sectionIndex in
let section = searchResultSet[sectionIndex]
let sectionTitle: String = {
switch section.model {
case .noResults:
return ""
case .contactsAndGroups:
return (section.elements.isEmpty ? "" : "CONVERSATION_SETTINGS_TITLE".localized())
case .messages:
return (section.elements.isEmpty ? "" : "SEARCH_SECTION_MESSAGES".localized())
case .defaultContacts:
return "NEW_CONVERSATION_CONTACTS_SECTION_TITLE".localized()
case .noResults:
return ""
case .contactsAndGroups:
return (section.elements.isEmpty ? "" : "CONVERSATION_SETTINGS_TITLE".localized())
case .messages:
return (section.elements.isEmpty ? "" : "SEARCH_SECTION_MESSAGES".localized())
case .defaultContacts:
return "NEW_CONVERSATION_CONTACTS_SECTION_TITLE".localized()
}
}()
if section.elements.count > 0 {
@ -71,40 +116,80 @@ struct GlobalSearchScreen: View {
.bold()
.font(.system(size: Values.mediumLargeFontSize))
.foregroundColor(themeColor: .textPrimary)
.padding(.horizontal, Values.mediumSpacing + Values.verySmallSpacing)
.padding(.top, Values.verySmallSpacing)
) {
ForEach(0..<section.elements.count, id: \.self) { rowIndex in
let rowViewModel = section.elements[rowIndex]
SearchResultCell(
searchText: searchText,
searchSection: section.model,
viewModel: rowViewModel
) {
show(
threadId: rowViewModel.threadId,
threadVariant: rowViewModel.threadVariant,
focusedInteractionInfo: {
guard
let interactionId: Int64 = rowViewModel.interactionId,
let timestampMs: Int64 = rowViewModel.interactionTimestampMs
else { return nil }
return Interaction.TimestampInfo(
id: interactionId,
timestampMs: timestampMs
)
}()
)
if section.model == .defaultContacts {
ForEach(0..<defaultGroupedContacts.count, id: \.self) { groupIndex in
let sectionData = defaultGroupedContacts[groupIndex]
Section(
header: Group{
if !sectionData.sectionName.isEmpty {
Text(sectionData.sectionName)
.font(.system(size: Values.smallFontSize))
.foregroundColor(themeColor: .textPrimary)
.padding(.horizontal, Values.mediumSpacing + Values.verySmallSpacing)
.padding(.top, Values.verySmallSpacing)
}
}
) {
ForEach(0..<sectionData.contacts.count, id: \.self) { rowIndex in
let rowViewModel = sectionData.contacts[rowIndex]
SearchResultCell(
searchText: searchText,
searchSection: section.model,
viewModel: rowViewModel
) {
show(
threadId: rowViewModel.threadId,
threadVariant: rowViewModel.threadVariant,
focusedInteractionInfo: {
guard
let interactionId: Int64 = rowViewModel.interactionId,
let timestampMs: Int64 = rowViewModel.interactionTimestampMs
else { return nil }
return Interaction.TimestampInfo(
id: interactionId,
timestampMs: timestampMs
)
}()
)
}
}
}
}
} else {
ForEach(0..<section.elements.count, id: \.self) { rowIndex in
let rowViewModel = section.elements[rowIndex]
SearchResultCell(
searchText: searchText,
searchSection: section.model,
viewModel: rowViewModel
) {
show(
threadId: rowViewModel.threadId,
threadVariant: rowViewModel.threadVariant,
focusedInteractionInfo: {
guard
let interactionId: Int64 = rowViewModel.interactionId,
let timestampMs: Int64 = rowViewModel.interactionTimestampMs
else { return nil }
return Interaction.TimestampInfo(
id: interactionId,
timestampMs: timestampMs
)
}()
)
}
}
.hideListRowSeparator()
}
}
}
}
}
.transparentScrolling()
.listStyle(.plain)
.padding(.top, -Values.mediumSpacing)
}
.backgroundColor(themeColor: .backgroundPrimary)
}
@ -249,6 +334,7 @@ struct SearchResultCell: View {
height: size.viewSize,
alignment: .topLeading
)
.padding(.vertical, Values.smallSpacing)
VStack(
alignment: .leading,
@ -307,7 +393,10 @@ struct SearchResultCell: View {
}
}
}
Spacer(minLength: 0)
}
.padding(.horizontal, Values.mediumSpacing)
}
}

@ -1781,6 +1781,50 @@ public extension SessionThreadViewModel {
}
}
static func defaultContactsQuery(userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contactProfile: TypedTableAlias<Profile> = TypedTableAlias(ViewModel.self, column: .contactProfile)
/// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before
/// the `contactProfile` entry below otherwise the query will fail to parse and might throw
let numColumnsBeforeProfiles: Int = 8
let request: SQLRequest<ViewModel> = """
SELECT
100 AS \(Column.rank),
\(thread[.rowId]) AS \(ViewModel.Columns.rowId),
\(thread[.id]) AS \(ViewModel.Columns.threadId),
\(thread[.variant]) AS \(ViewModel.Columns.threadVariant),
\(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp),
'' AS \(ViewModel.Columns.threadMemberNames),
(\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf),
IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority),
\(contactProfile.allColumns),
\(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey)
FROM \(SessionThread.self)
JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id])
WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)"))
"""
// Add adapters which will group the various 'Profile' columns so they can be decoded
// as instances of 'Profile' types
return request.adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
numColumnsBeforeProfiles,
Profile.numberOfSelectedColumns(db)
])
return ScopeAdapter.with(ViewModel.self, [
.contactProfile: adapters[1]
])
}
}
/// This method returns only the 'Note to Self' thread in the structure of a search result conversation
static func noteToSelfOnlyQuery(userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()

@ -0,0 +1,26 @@
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
public struct CompatibleScrollingVStack<Content> : View where Content : View {
let alignment: HorizontalAlignment
let spacing: CGFloat?
let content: () -> Content
public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil,
@ViewBuilder content: @escaping () -> Content) {
self.alignment = alignment
self.spacing = spacing
self.content = content
}
public var body: some View {
ScrollView {
if #available(iOS 14, *) {
LazyVStack(alignment: alignment, spacing: spacing, pinnedViews: [], content:content)
} else {
VStack(alignment: alignment, spacing: spacing, content:content)
}
}
}
}

@ -68,7 +68,7 @@ public struct SessionTextField<ExplanationView>: View where ExplanationView: Vie
)
.font(.system(size: Values.smallFontSize))
.foregroundColor(themeColor: textThemeColor)
.transparentScrolling()
.textViewTransparentScrolling()
.frame(maxHeight: self.height)
.padding(.all, -4)

@ -103,7 +103,7 @@ extension View {
self.modifier(ToastModifier(message: message))
}
public func transparentScrolling() -> some View {
public func textViewTransparentScrolling() -> some View {
if #available(iOS 16.0, *) {
return scrollContentBackground(.hidden)
} else {
@ -113,6 +113,16 @@ extension View {
}
}
public func transparentListBackground() -> some View {
if #available(iOS 16.0, *) {
return scrollContentBackground(.hidden)
} else {
return onAppear {
UITableView.appearance().backgroundColor = .clear
}
}
}
public func hideListRowSeparator() -> some View {
if #available(iOS 15.0, *) {
return listRowSeparator(.hidden)

Loading…
Cancel
Save