Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -464,10 +464,11 @@ private extension WireCellsGetNodesRequest {
deleted: .not,
isDraft: false
),
text: LookupFilterTextSearch(searchIn: .baseName, term: searchTerm ?? "*"),
type: isFoldersEnabled ? .unknown : .leaf // .unknown includes files (leafs) & folders (collections)
)
request.scope = RestLookupScope(
recursive: isFoldersEnabled ? false : true,
recursive: searchTerm?.isEmpty == false,
root: RestNodeLocator(root)
)
case let .recycleBinView(root: root, isFoldersEnabled):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct FilenameValidator {
.failure(.dotPrefix)
} else if input.count > Constants.maxInputLength {
.failure(.tooLong)
} else if input.contains("/") {
} else if input.contains("/") || input.contains("\\") {
.failure(.slashCharacter)
} else {
.success(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private extension FilesBrowserView {

// MARK: - Helper

private extension View {
extension View {
@ViewBuilder
func `if`(
_ condition: Bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ package struct FilesView: FilesViewProtocol {
.toolbarBackground(.visible, for: .navigationBar) // shows navigation bar divider
.toolbarBackground(ColorTheme.Backgrounds.background.color, for: .navigationBar)
.toolbar { toolbarContent }
.if(viewModel.showSearchBar) { view in
view.searchable(
text: $viewModel.searchText,
placement: .navigationBarDrawer,
prompt: Strings.Files.Search.title
)
}
.onAppear { reloadTask() }
.onReceive(viewModel.triggerReload) { _ in
Task {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions WireUI/Sources/WireDesign/Colors/ColorTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,16 @@
public static let onTransparentDark = UIColor(light: .white, dark: .white)
}

public enum Banners {

Check warning on line 105 in WireUI/Sources/WireDesign/Colors/ColorTheme.swift

View workflow job for this annotation

GitHub Actions / Test Results

Static property 'background' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode

Static property 'background' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode
public static let background = Base.primaryVariant

Check warning on line 106 in WireUI/Sources/WireDesign/Colors/ColorTheme.swift

View workflow job for this annotation

GitHub Actions / Test Results

Static property 'border' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode

Static property 'border' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode
public static let border = Base.primary
}

public enum Buttons {

enum Primary {
public enum Primary {

static let enabled = UIColor(light: .blue500Light, dark: .blue500Dark)
public static let enabled = UIColor(light: .blue500Light, dark: .blue500Dark)
static let onEnabled = UIColor(light: .white, dark: .black)

static let disabled = UIColor(light: .gray50, dark: .gray80)
Expand Down Expand Up @@ -166,9 +166,9 @@
public static let onEnabled = Backgrounds.onSurface

static let onDisabled = Base.secondaryText

Check warning on line 169 in WireUI/Sources/WireDesign/Colors/ColorTheme.swift

View workflow job for this annotation

GitHub Actions / Test Results

Static property 'onFocus' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode

Static property 'onFocus' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode
static let onFocus = Base.primary

Check warning on line 171 in WireUI/Sources/WireDesign/Colors/ColorTheme.swift

View workflow job for this annotation

GitHub Actions / Test Results

Static property 'onSelected' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode

Static property 'onSelected' is not concurrency-safe because non-'Sendable' type '(WireAccentColor) -> UIColor' may have shared mutable state; this is an error in the Swift 6 language mode
static let onSelected = Base.primary
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ final class CollectionsViewControllerTests: XCTestCase {
func testThatNoElementStateIsShownWhenCollectionIsEmpty() {
let controller = CollectionsViewController(
collection: emptyCollection,
isCellsEnabled: false,
fetchingDone: true,
userSession: userSession,
mainCoordinator: mockMainCoordinator,
Expand All @@ -138,6 +139,7 @@ final class CollectionsViewControllerTests: XCTestCase {
func testThatLoadingIsShownWhenFetching() {
let controller = CollectionsViewController(
collection: emptyCollection,
isCellsEnabled: false,
fetchingDone: false,
userSession: userSession,
mainCoordinator: mockMainCoordinator,
Expand Down Expand Up @@ -244,6 +246,7 @@ final class CollectionsViewControllerTests: XCTestCase {

let controller = CollectionsViewController(
collection: collection,
isCellsEnabled: false,
userSession: userSession,
mainCoordinator: mockMainCoordinator,
selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(),
Expand Down
10 changes: 10 additions & 0 deletions wire-ios/Wire-iOS/Generated/Strings+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,16 @@ internal enum L10n {
/// Links
internal static let title = L10n.tr("Localizable", "collections.section.links.title", fallback: "Links")
}
internal enum SearchFiles {
/// Search files
internal static let description = L10n.tr("Localizable", "collections.section.searchFiles.description", fallback: "Search files")
internal enum Alert {
/// Find files shared in conversations with file collaboration
internal static let message = L10n.tr("Localizable", "collections.section.searchFiles.alert.message", fallback: "Find files shared in conversations with file collaboration")
/// File collaboration (Cells beta version)
internal static let title = L10n.tr("Localizable", "collections.section.searchFiles.alert.title", fallback: "File collaboration (Cells beta version)")
}
}
internal enum Videos {
/// Videos
internal static let title = L10n.tr("Localizable", "collections.section.videos.title", fallback: "Videos")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1868,6 +1868,10 @@ Enter your identity provider’s credentials in the next step to update the cert
"collections.search.field.placeholder" = "Search text messages";
"collections.search.no_items" = "No results";

"collections.section.searchFiles.description" = "Search files";
"collections.section.searchFiles.alert.title" = "File collaboration (Cells beta version)";
"collections.section.searchFiles.alert.message" = "Find files shared in conversations with file collaboration";



// +++++++++++++++++++++++++++++++++++++++++++++++++++++
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// 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 http://www.gnu.org/licenses/.
//

import UIKit
import WireCommonComponents
import WireDataModel
import WireDesign
import WireFoundation

final class CollectionSearchFilesCell: CollectionCell {
private let containerView = UIView()

private let label: UILabel = {
let label = UILabel()
label.text = L10n.Localizable.Collections.Section.SearchFiles.description
return label
}()

private let chevronImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(systemName: "chevron.right"))
imageView.tintColor = ColorTheme.Base.secondaryText
return imageView
}()

private let informationButton: UIButton = {
var config = UIButton.Configuration.plain()
config.image = UIImage(systemName: "info.circle")
config.baseForegroundColor = ColorTheme.Buttons.Primary.enabled

return UIButton(configuration: config)
}()

@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override init(frame: CGRect) {
super.init(frame: frame)
loadView()
}

private func loadView() {
let views = [label, containerView, chevronImageView, informationButton]
views.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
views.forEach(secureContentsView.addSubview)
secureContentsView.layer.cornerRadius = 16

NSLayoutConstraint.activate([
// label
label.topAnchor.constraint(equalTo: secureContentsView.topAnchor),
label.bottomAnchor.constraint(equalTo: secureContentsView.bottomAnchor),
label.leadingAnchor.constraint(equalTo: secureContentsView.leadingAnchor, constant: 16),
label.trailingAnchor.constraint(greaterThanOrEqualTo: informationButton.trailingAnchor),

// information button
informationButton.heightAnchor.constraint(equalToConstant: 18),
informationButton.widthAnchor.constraint(equalToConstant: 18),
informationButton.trailingAnchor.constraint(equalTo: chevronImageView.leadingAnchor, constant: -16),
informationButton.centerYAnchor.constraint(equalTo: secureContentsView.centerYAnchor),

// chevron icon
chevronImageView.heightAnchor.constraint(equalToConstant: 20),
chevronImageView.widthAnchor.constraint(equalToConstant: 12),
chevronImageView.centerYAnchor.constraint(equalTo: secureContentsView.centerYAnchor),
chevronImageView.trailingAnchor.constraint(equalTo: secureContentsView.trailingAnchor, constant: -16),

// containerView
containerView.topAnchor.constraint(equalTo: secureContentsView.bottomAnchor, constant: 4),
containerView.leadingAnchor.constraint(equalTo: secureContentsView.leadingAnchor, constant: 4),
containerView.trailingAnchor.constraint(equalTo: secureContentsView.trailingAnchor, constant: 4),
containerView.bottomAnchor.constraint(equalTo: secureContentsView.bottomAnchor, constant: 4)
])
}

func configure(
accentColor: WireAccentColor?,
informationButtonHandler: @escaping () -> Void
) {
if let accentColor {
var config = UIButton.Configuration.plain()
config.image = UIImage(systemName: "info.circle")
config.baseForegroundColor = ColorTheme.Base.primary(accentColor)
informationButton.configuration = config
}
let action = UIAction { _ in informationButtonHandler() }
informationButton.addAction(action, for: .touchUpInside)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ struct CollectionsSectionSet: OptionSet, Hashable {
self.rawValue = rawValue
}

init?(index: UInt) {
self = type(of: self).visible[Int(index)]
init?(index: UInt, isCellsEnabled: Bool) {
self = isCellsEnabled ? type(of: self).visibleWithSearchFiles[Int(index)] : type(of: self).visible[Int(index)]
}

static let none = CollectionsSectionSet([])
Expand All @@ -37,10 +37,14 @@ struct CollectionsSectionSet: OptionSet, Hashable {
static let videos = CollectionsSectionSet(rawValue: 1 << 2)
static let links = CollectionsSectionSet(rawValue: 1 << 3)
static let loading = CollectionsSectionSet(rawValue: 1 << 4) // special section that shows the loading view
static let searchFiles = CollectionsSectionSet(rawValue: 1 << 5)

/// Returns all possible section types
static let all: CollectionsSectionSet = [.images, .filesAndAudio, .videos, .links, .loading]

/// Returns visible sections in the display order
static let visible: [CollectionsSectionSet] = [images, videos, links, filesAndAudio, loading]

/// Returns visible sections with search files in the display order
static let visibleWithSearchFiles = [CollectionsSectionSet.searchFiles] + CollectionsSectionSet.visible
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ final class CollectionsView: UIView {
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: CollectionHeaderView.reuseIdentifier
)
collectionView.register(
CollectionSearchFilesCell.self,
forCellWithReuseIdentifier: CollectionSearchFilesCell.reuseIdentifier
)
collectionView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.allowsMultipleSelection = false
Expand Down
Loading
Loading