Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
4596b7a
chore: update DB version with new attributes on `WireCellsLocalAsset`…
jullianm Mar 27, 2026
5b1cf5c
feat: add button action for file offline availability (UI only) - WPB…
jullianm Mar 30, 2026
7cf32de
removing UI elements for offline mode, changing No-Internet bar
WilhelmOks Mar 31, 2026
6a02c84
file item menu actions checking for offline mode
WilhelmOks Mar 31, 2026
0a9897d
offline mode: create use cases, adjust repository logic and views acc…
jullianm Mar 31, 2026
2a75ef0
refactored NetworkMonitor to be Observable instead of just exposing a…
WilhelmOks Mar 31, 2026
4d31b4c
removed @preconcurrency from import Combine
WilhelmOks Mar 31, 2026
79ae43a
Merge branch 'feat/WPB-23967-make-files-available-offline' into feat/…
WilhelmOks Apr 1, 2026
01ccc4c
removed unused property: showOfflineDownload
WilhelmOks Apr 1, 2026
685ffd0
Merge branch 'develop' into feat/WPB-23967-make-files-available-offline
jullianm Apr 1, 2026
40f38d3
fix remaining merge conflicts, adjust offline mode UI code following …
jullianm Apr 1, 2026
61b4a32
add conditional check on downloaded state
jullianm Apr 1, 2026
aba8521
loading list of offline files (one dummy file for now) when offline m…
WilhelmOks Apr 1, 2026
00bf131
add localized string
jullianm Apr 1, 2026
8a3438c
fix compilation issue
jullianm Apr 1, 2026
9130c0d
Merge branch 'feat/WPB-23967-make-files-available-offline' into feat/…
WilhelmOks Apr 1, 2026
586b87e
add DEBUG action to delete asset from db and cache
jullianm Apr 1, 2026
089c93e
lint and format
jullianm Apr 1, 2026
7df2770
don't automatically open file when file is made available offline
jullianm Apr 1, 2026
b548c28
fix UTs
jullianm Apr 2, 2026
56a811a
Merge branch 'feat/WPB-23967-make-files-available-offline' into feat/…
WilhelmOks Apr 2, 2026
c4effdc
moved the DEBUG button to the end of the list
WilhelmOks Apr 2, 2026
1a8c631
removed the reference to bindNetworkConnection in a comment
WilhelmOks Apr 2, 2026
12e912e
moved the DEBUG button to the end of the list
WilhelmOks Apr 2, 2026
edf6609
add UTs
jullianm Apr 2, 2026
a7bf499
correct design for the offline bar (except for the color, which is a …
WilhelmOks Apr 2, 2026
6316025
add condition checks to display actions, remove available offline fla…
jullianm Apr 2, 2026
dee0dd7
reverse asset deletion code order (first delete asset locally then in…
jullianm Apr 2, 2026
a5ce45c
Merge branch 'feat/WPB-23967-make-files-available-offline' into feat/…
WilhelmOks Apr 2, 2026
edaa8fd
fetching offline assets in offline mode
WilhelmOks Apr 2, 2026
0c034a5
added offline bar to empty state
WilhelmOks Apr 2, 2026
72ac7aa
fetch offline assets from db if don't already exist in memory, fix in…
jullianm Apr 3, 2026
9370d90
lint and format
jullianm Apr 3, 2026
2e00762
add logic to fetch only assets of a given conversation, introduced a …
jullianm Apr 3, 2026
d20d863
fix UTs compilation issue
jullianm Apr 7, 2026
eff5b36
allow mocking NWPathMonitor to fix UTs
jullianm Apr 7, 2026
fb9e4fd
sorting offline files by FS creation date
WilhelmOks Apr 7, 2026
c82f2af
skipping reload when the network status isnt available yet, but witho…
WilhelmOks Apr 7, 2026
4e6f9f5
feat: re-create folder structure locally when offline - WPB-24437 (#4…
jullianm Apr 14, 2026
d062c31
create async version of asset update, use it when removing a file ava…
jullianm Apr 14, 2026
012c5f8
use .path instead of .id for the cell root, don't load offline files …
jullianm Apr 15, 2026
82cc1af
Merge branch 'develop' into feat/WPB-23967-make-files-available-offline
jullianm Apr 15, 2026
87758e2
avoid code duplication
jullianm Apr 15, 2026
35127e9
Merge branch 'develop' into feat/WPB-23967-make-files-available-offline
jullianm Apr 15, 2026
1e92ef5
factor out view builder method in UT
jullianm Apr 15, 2026
3a764c6
Merge branch 'develop' into feat/WPB-23967-make-files-available-offline
jullianm Apr 16, 2026
790a8e8
remove available offline icon on folder
jullianm Apr 16, 2026
d688188
fix file opening automatically after download when made available off…
jullianm Apr 16, 2026
43c690a
explicitly nil out viewingURL before opening the asset
jullianm Apr 16, 2026
2cd4f35
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 20, 2026
6616c4c
moving FilesSortingViewModel.SortingKey and WireDriveFileTemplate.Kin…
WilhelmOks Apr 16, 2026
b353407
removing duplicate file
WilhelmOks Apr 20, 2026
7301362
moved code related to offline files to OfflineAvailableFilesHandler
WilhelmOks Apr 20, 2026
aeb2f6a
using UIActions to encapsulate single actions rather than groups of a…
WilhelmOks Apr 21, 2026
1a4eb6f
refactored processItems()
WilhelmOks Apr 21, 2026
fc67e71
extracted temporary workaround hardcoded templates fetching from the …
WilhelmOks Apr 21, 2026
7f6303d
refactored file creation notification from Combine Publisher to callb…
WilhelmOks Apr 21, 2026
ed533c3
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 22, 2026
0d5de3f
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 23, 2026
b6a3f1f
refactoring FileVersioningViewModel notification mechanism to a closu…
WilhelmOks Apr 23, 2026
be45dcc
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 24, 2026
cf231bf
added .quickLookPreview($viewModel.viewingURL) to Versioning View aga…
WilhelmOks Apr 24, 2026
bdffa97
refactoring notification mechanism to a closure
WilhelmOks Apr 24, 2026
85c7729
extracted some types into separate files
WilhelmOks Apr 24, 2026
58f1dd2
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 24, 2026
06664f4
refactored SheetNavigation to not use View as the payload
WilhelmOks Apr 24, 2026
74127c1
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 24, 2026
d47d7ca
refactored creation of multiple Views so that import SwiftUI isn't re…
WilhelmOks Apr 24, 2026
fb28997
moving all functions which create ViewModels into a separate file
WilhelmOks Apr 24, 2026
0675d33
moved WireDriveNode-to-FilesViewItem mapping function to FilesViewItem
WilhelmOks Apr 27, 2026
613e71e
removed fileCache: any FileCache (not needed)
WilhelmOks Apr 27, 2026
0750a76
cleanup, reordering, grouping
WilhelmOks Apr 27, 2026
cd627b3
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 27, 2026
9c07265
removing a TODO
WilhelmOks Apr 27, 2026
afa31e5
extracted logic of paginated files loading into dedicated components
WilhelmOks Apr 28, 2026
8a938a9
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 28, 2026
c8b5367
lint&format
WilhelmOks Apr 29, 2026
6df21c2
lint&format fetchTemplates
WilhelmOks Apr 29, 2026
202084e
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks Apr 29, 2026
d085887
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks May 5, 2026
f2ad360
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks May 5, 2026
b0c15ae
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks May 6, 2026
a0bf1e7
Merge branch 'develop' into improvement/WPB-24805-FilesViewModel-2
WilhelmOks May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ public extension WireMessagingFactory {
localAssetRepository: localAssetRepository,
nodeCache: nodeCache,
nodeRenameNotifier: nodeRenameNotifier,
fileCache: fileCache,
accentColorProvider: accentColorProvider
).environment(\.wireAccentColor, accentColorProvider())
fileCache: fileCache
)
.environment(\.wireAccentColor, accentColorProvider())
)
}

Expand Down Expand Up @@ -216,6 +216,7 @@ public extension WireMessagingFactory {
updatePublicLinkExpiration: WireDriveUpdatePublicLinkExpirationUseCase(nodesAPI: nodesAPI),
updatePublicLinkPassword: WireDriveUpdatePublicLinkPasswordUseCase(nodesAPI: nodesAPI),
getDriveConversations: WireDriveGetConversationsUseCase(nodesAPI: nodesAPI),
getFileTemplates: WireDriveFetchFileTemplatesUseCase(repository: nodesAPI),
makeAssetAvailableOffline: WireDriveMakeAssetAvailableOfflineUseCase(
localAssetRepository: localAssetRepository
),
Expand All @@ -229,11 +230,10 @@ public extension WireMessagingFactory {
isCellsStatePending: false,
localAssetRepository: localAssetRepository,
nodesRepository: nodesAPI,
fileCache: fileCache,
isBrowsing: true,
accentColorProvider: accentColorProvider
isBrowsing: true
)
).environment(\.wireAccentColor, accentColorProvider())
)
.environment(\.wireAccentColor, accentColorProvider())
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,18 @@ package enum WireDriveCreateFileUseCaseError: Error {
/// Creates a file or a folder on the server.
package struct WireDriveCreateFileUseCase: WireDriveCreateFileUseCaseProtocol {

package enum Target: Equatable {
package enum Target: Equatable, Identifiable {
case folder
case file(WireDriveFileTemplate)

package var id: String {
switch self {
case .folder:
"folder"
case let .file(template):
"file:\(template.id)"
}
}
}

private let nodesRepository: any WireDriveNodesRepositoryProtocol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

package import Foundation
import Foundation

/// Fetches templates useable for document creation (.docx, .pptx, .xlsx. etc)
package struct WireDriveFetchFileTemplatesUseCase: WireDriveFetchFileTemplatesUseCaseProtocol {
Expand All @@ -30,7 +30,28 @@
}

package func invoke() async throws -> [WireDriveFileTemplate] {
try await repository.getTemplates()
// TODO: [WPB-22926] Replace hard coded values with server values when GET/ templates endpoint ready.

Check warning on line 33 in WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/WireDriveFetchFileTemplatesUseCase.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ2wuuDdvn5gzXa5dM8n&open=AZ2wuuDdvn5gzXa5dM8n&pullRequest=4590
// Do `try await repository.getTemplates()`
[
.init(
kind: .document,
editable: true,
label: "Microsoft Word",
id: "01-Microsoft Word.docx"
),
.init(
kind: .spreadsheet,
editable: true,
label: "Microsoft Excel",
id: "02-Microsoft Excel.xlsx"
),
.init(
kind: .presentation,
editable: true,
label: "Microsoft PowerPoint",
id: "03-Microsoft PowerPoint.pptx"
)
]
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// 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 Combine
import Foundation
import Observation
import WireLogging

/// Loads and manages a paginated list of `FilesViewItem` elements.
@MainActor
class FilesListLoader: Observable, ObservableObject {
typealias Loader = IncrementalListLoader<FilesViewItem>

@Published private(set) var networkMonitor: NetworkMonitor
@Published private(set) var loader: Loader

var onFetchOnlineFiles: ((Int) async throws -> (items: [FilesViewItem], isLastPage: Bool))?
var onFetchOfflineFiles: (() async throws -> [FilesViewItem])?
var onErrorToPresent: ((any Error) -> Void)?

init(networkMonitor: NetworkMonitor = .shared) {
self.networkMonitor = networkMonitor

self.loader = .init()
loader.onFetch = { [weak self] offset in
guard let self else { return (items: [], isLastPage: true) }

if isOffline {
let items = try await onFetchOfflineFiles?() ?? []
return (items: items, isLastPage: true)
} else {
let (items, isLastPage) = try await onFetchOnlineFiles?(offset) ?? ([], true)
return (items: items.latestModified(), isLastPage: isLastPage)
}
}
loader.onError = { [weak self] error in
guard let self else { return }

if isOffline {
WireLogger.wireDrive.error("Error fetching offline assets:\n\(error)")
onErrorToPresent?(error)
} else {
if loader.state.items.isEmpty {
loader.state = .error(isConnectionError: error.isNoInternetError)
} else {
if !error.isNoInternetError {
WireLogger.wireDrive.error("Error fetching online files:\n\(error)")
onErrorToPresent?(error)
}
}
}
}
}

private var isOffline: Bool {
networkMonitor.currentStatus == .disconnected
}

/// Removes an item from the list, then performs an async action.
/// If the action fails, restores the list to how it was before removing the item.
func removeItem(_ item: FilesViewItem, withAction action: @escaping () async throws -> Void) async {
let items = loader.state.items
let changedItems = items.filter { $0.id != item.id }
loader.state = .received(items: changedItems)

do {
try await action()
} catch {
guard loader.state.isLoaded else { return }

// set items to how they were before the change:
loader.state = .received(items: items)
}
}
}

private extension Collection<FilesViewItem> {
/// Removes items with duplicate IDs keeping the latest modified if known, otherwise the first.
func latestModified() -> [FilesViewItem] {
var latestByID: [UUID: FilesViewItem] = [:]
for item in self {
if let existing = latestByID[item.id] {
let existingDate = existing.modifiedAt ?? .distantPast
let newDate = item.modifiedAt ?? .distantPast
if newDate > existingDate {
latestByID[item.id] = item
}
} else {
latestByID[item.id] = item
}
}

var results: [FilesViewItem] = []
for item in self where item == latestByID[item.id] {
results.append(item)
}

return results
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// 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 Combine
import Observation

/// A generic loader for paginated lists of items.
@MainActor
class IncrementalListLoader<Item: Identifiable & Hashable & Sendable>: Observable, ObservableObject {
private typealias LoadTask = Task<(items: [Item], isLastPage: Bool), any Error>
typealias ItemsData = (items: [Item], isLastPage: Bool)

enum State: Hashable {
case loading
case received(items: [Item])
case pending
case error(isConnectionError: Bool)

var items: [Item] {
switch self {

Check warning on line 35 in WireMessaging/Sources/WireMessagingUI/WireDrive/Components/Common/IncrementalListLoader.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this "switch" statement with "if" statement to increase readability.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ3TkhcYzI5R9QBPYsCU&open=AZ3TkhcYzI5R9QBPYsCU&pullRequest=4590
case let .received(items):
items
default:
[]
}
}

var isLoaded: Bool {
switch self {
case .loading, .pending, .error:
false
case .received:
true
}
}
}

/// How close to the end of the list before loading more items.
var loadMoreThreshold = 5

@Published var state: State = .pending
@Published var hasMore = true

private var loadTask: LoadTask?

var onFetch: ((Int) async throws -> ItemsData)?
var onError: ((any Error) -> Void)?

/// Reloads the items, clearing any previously loaded items.
/// - Parameters:
/// - refreshing: Whether the reload was triggered by a pull-to-refresh action.
///
/// Cancels any ongoing load operation and starts a new one.
/// When `refreshing` is `true`, the current state is preserved since loading is managed by the system.
func reload(refreshing: Bool = false) async {
cancelLoad()
state = refreshing ? state : .loading
hasMore = !refreshing

await loadMore(refreshing: refreshing)
}

/// Loads more items if available and `index` is towards the end of the list.
///
/// This method checks if the `index` is within the threshold for loading more items. For example given a threshold
/// of 5, when 10 items are loaded, it will load more when the index is 5 or above - i.e. when one of the last 5
/// items is being displayed.
///
/// - Parameter index: The index of the item which requested load more.
func loadMoreIfNeeded(index: Int) async {
let remaining = state.items.count - index - 1
if remaining < loadMoreThreshold, hasMore {
await loadMore()
}
}

var isLoading: Bool {
loadTask != nil
}
}

private extension IncrementalListLoader {
func cancelLoad() {
loadTask?.cancel()
loadTask = nil
}

func loadMore(refreshing: Bool = false) async {
guard loadTask == nil else { return }

let offset = refreshing ? 0 : state.items.count
let task = Task {
try await fetchItems(offset: offset)
}

loadTask = task
defer { loadTask = nil }

do {
let (newItems, isLastPage) = try await task.value
let receivedItems = refreshing ? newItems : state.items + newItems
state = .received(items: receivedItems)
hasMore = !isLastPage
} catch is CancellationError {
return // developer-driven error, discard
} catch {
hasMore = state.items.isEmpty ? true : hasMore
onError?(error)
}
}

func fetchItems(offset: Int) async throws -> ItemsData {
if let onFetch {
let itemsData = try await onFetch(offset)
try Task.checkCancellation()
return itemsData
} else {
return (items: [], isLastPage: true)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ final class CreateFileViewModel: ObservableObject {
@Published var errorMessage: String?
@Published var isLoading: Bool = false
@Published var isFocused: Bool = true
@Published var createdNode: WireDriveNode?

let onNodeCreated: (WireDriveNode) -> Void

var isCreateDisabled: Bool {
errorMessage != nil || !isInputValid
Expand Down Expand Up @@ -107,10 +108,12 @@ final class CreateFileViewModel: ObservableObject {
creationTarget: WireDriveCreateFileUseCase.Target,
path: String,
createFileUseCase: any WireDriveCreateFileUseCaseProtocol,
onNodeCreated: @escaping (WireDriveNode) -> Void
) {
self.creationTarget = creationTarget
self.createFileUseCase = createFileUseCase
self.path = path
self.onNodeCreated = onNodeCreated

bindTextInput()
}
Expand All @@ -121,12 +124,14 @@ final class CreateFileViewModel: ObservableObject {
do {
isLoading = true

createdNode = try await createFileUseCase.invoke(
let createdNode = try await createFileUseCase.invoke(
creationTarget: creationTarget,
path: path,
name: nameInput.trimmingCharacters(in: .whitespacesAndNewlines)
)

onNodeCreated(createdNode)

isLoading = false

return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ private typealias Accessibility = L10n.Accessibility.Conversation.WireCells

struct FileVersionItemView: View {
@StateObject private var viewModel: FileVersionItemViewModel
@Environment(\.wireAccentColor) private var wireAccentColor
@State private var showRestoreVersionAlert = false

init(
Expand Down
Loading
Loading