mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Public Search View (#2585)
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
@@ -53,6 +53,7 @@ final class AppSettings {
|
||||
case markAsUnreadEnabled
|
||||
case markAsFavouriteEnabled
|
||||
case roomModerationEnabled
|
||||
case publicSearchEnabled
|
||||
}
|
||||
|
||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
@@ -283,6 +284,9 @@ final class AppSettings {
|
||||
@UserPreference(key: UserDefaultsKeys.roomModerationEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var roomModerationEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.publicSearchEnabled, defaultValue: false, storageType: .volatile)
|
||||
var publicSearchEnabled
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - Shared
|
||||
|
||||
@@ -310,6 +310,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (.logoutConfirmationScreen, .dismissedLogoutConfirmationScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .showRoomDirectorySearchScreen, .roomDirectorySearchScreen):
|
||||
presentRoomDirectorySearch()
|
||||
case (.roomDirectorySearchScreen, .dismissedRoomDirectorySearchScreen, .roomList):
|
||||
dismissRoomDirectorySearch()
|
||||
|
||||
default:
|
||||
fatalError("Unknown transition: \(context)")
|
||||
}
|
||||
@@ -370,6 +375,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.processEvent(.showInvitesScreen)
|
||||
case .presentGlobalSearch:
|
||||
presentGlobalSearch()
|
||||
case .presentRoomDirectorySearch:
|
||||
stateMachine.processEvent(.showRoomDirectorySearchScreen)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -566,4 +573,26 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
globalSearchScreenCoordinator = nil
|
||||
}
|
||||
|
||||
// MARK: Room Directory Search
|
||||
|
||||
private func presentRoomDirectorySearch() {
|
||||
let coordinator = RoomDirectorySearchScreenCoordinator(parameters: .init(roomDirectorySearchProxy: userSession.clientProxy.roomDirectorySearchProxy(),
|
||||
imageProvider: userSession.mediaProvider,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self?.stateMachine.processEvent(.dismissedRoomDirectorySearchScreen(joinedRoomID: nil))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func dismissRoomDirectorySearch() {
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
// Showing the logout flows
|
||||
case logoutConfirmationScreen(selectedRoomID: String?)
|
||||
|
||||
// Showing Room Directory Search screen
|
||||
case roomDirectorySearchScreen(selectedRoomID: String?)
|
||||
}
|
||||
|
||||
struct EventUserInfo {
|
||||
@@ -81,6 +84,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
case showLogoutConfirmationScreen
|
||||
/// Logout has been cancelled
|
||||
case dismissedLogoutConfirmationScreen
|
||||
|
||||
case showRoomDirectorySearchScreen
|
||||
case dismissedRoomDirectorySearchScreen(joinedRoomID: String?)
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
@@ -136,6 +142,14 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
case (.logoutConfirmationScreen(let selectedRoomID), .dismissedLogoutConfirmationScreen):
|
||||
return .roomList(selectedRoomID: selectedRoomID)
|
||||
|
||||
case (.roomList(let selectedRoomID), .showRoomDirectorySearchScreen):
|
||||
return .roomDirectorySearchScreen(selectedRoomID: selectedRoomID)
|
||||
case (.roomDirectorySearchScreen(let selectedRoomID), .dismissedRoomDirectorySearchScreen(let joinedRoomID)):
|
||||
if let joinedRoomID {
|
||||
return .roomList(selectedRoomID: joinedRoomID)
|
||||
}
|
||||
return .roomList(selectedRoomID: selectedRoomID)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1311,6 +1311,10 @@ internal enum L10n {
|
||||
internal static var screenRoomDetailsTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_topic_title") }
|
||||
/// Updating room…
|
||||
internal static var screenRoomDetailsUpdatingRoom: String { return L10n.tr("Localizable", "screen_room_details_updating_room") }
|
||||
/// Failed loading
|
||||
internal static var screenRoomDirectorySearchLoadingError: String { return L10n.tr("Localizable", "screen_room_directory_search_loading_error") }
|
||||
/// Room directory
|
||||
internal static var screenRoomDirectorySearchTitle: String { return L10n.tr("Localizable", "screen_room_directory_search_title") }
|
||||
/// Message history is currently unavailable.
|
||||
internal static var screenRoomEncryptedHistoryBanner: String { return L10n.tr("Localizable", "screen_room_encrypted_history_banner") }
|
||||
/// Message history is unavailable in this room. Verify this device to see your message history.
|
||||
@@ -1543,6 +1547,8 @@ internal enum L10n {
|
||||
internal static var screenRoomlistMarkAsRead: String { return L10n.tr("Localizable", "screen_roomlist_mark_as_read") }
|
||||
/// Mark as unread
|
||||
internal static var screenRoomlistMarkAsUnread: String { return L10n.tr("Localizable", "screen_roomlist_mark_as_unread") }
|
||||
/// Browse all rooms
|
||||
internal static var screenRoomlistRoomDirectoryButtonTitle: String { return L10n.tr("Localizable", "screen_roomlist_room_directory_button_title") }
|
||||
/// Change account provider
|
||||
internal static var screenServerConfirmationChangeServer: String { return L10n.tr("Localizable", "screen_server_confirmation_change_server") }
|
||||
/// A private server for Element employees.
|
||||
|
||||
@@ -1131,6 +1131,23 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
return profileForReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - roomDirectorySearchProxy
|
||||
|
||||
var roomDirectorySearchProxyCallsCount = 0
|
||||
var roomDirectorySearchProxyCalled: Bool {
|
||||
return roomDirectorySearchProxyCallsCount > 0
|
||||
}
|
||||
var roomDirectorySearchProxyReturnValue: RoomDirectorySearchProxyProtocol!
|
||||
var roomDirectorySearchProxyClosure: (() -> RoomDirectorySearchProxyProtocol)?
|
||||
|
||||
func roomDirectorySearchProxy() -> RoomDirectorySearchProxyProtocol {
|
||||
roomDirectorySearchProxyCallsCount += 1
|
||||
if let roomDirectorySearchProxyClosure = roomDirectorySearchProxyClosure {
|
||||
return roomDirectorySearchProxyClosure()
|
||||
} else {
|
||||
return roomDirectorySearchProxyReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - ignoreUser
|
||||
|
||||
var ignoreUserCallsCount = 0
|
||||
@@ -2245,6 +2262,52 @@ class PollInteractionHandlerMock: PollInteractionHandlerProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
class RoomDirectorySearchProxyMock: RoomDirectorySearchProxyProtocol {
|
||||
var resultsPublisher: CurrentValuePublisher<[RoomDirectorySearchResult], Never> {
|
||||
get { return underlyingResultsPublisher }
|
||||
set(value) { underlyingResultsPublisher = value }
|
||||
}
|
||||
var underlyingResultsPublisher: CurrentValuePublisher<[RoomDirectorySearchResult], Never>!
|
||||
|
||||
//MARK: - search
|
||||
|
||||
var searchQueryCallsCount = 0
|
||||
var searchQueryCalled: Bool {
|
||||
return searchQueryCallsCount > 0
|
||||
}
|
||||
var searchQueryReceivedQuery: String?
|
||||
var searchQueryReceivedInvocations: [String?] = []
|
||||
var searchQueryReturnValue: Result<Void, RoomDirectorySearchError>!
|
||||
var searchQueryClosure: ((String?) async -> Result<Void, RoomDirectorySearchError>)?
|
||||
|
||||
func search(query: String?) async -> Result<Void, RoomDirectorySearchError> {
|
||||
searchQueryCallsCount += 1
|
||||
searchQueryReceivedQuery = query
|
||||
searchQueryReceivedInvocations.append(query)
|
||||
if let searchQueryClosure = searchQueryClosure {
|
||||
return await searchQueryClosure(query)
|
||||
} else {
|
||||
return searchQueryReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - nextPage
|
||||
|
||||
var nextPageCallsCount = 0
|
||||
var nextPageCalled: Bool {
|
||||
return nextPageCallsCount > 0
|
||||
}
|
||||
var nextPageReturnValue: Result<Void, RoomDirectorySearchError>!
|
||||
var nextPageClosure: (() async -> Result<Void, RoomDirectorySearchError>)?
|
||||
|
||||
func nextPage() async -> Result<Void, RoomDirectorySearchError> {
|
||||
nextPageCallsCount += 1
|
||||
if let nextPageClosure = nextPageClosure {
|
||||
return await nextPageClosure()
|
||||
} else {
|
||||
return nextPageReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class RoomMemberProxyMock: RoomMemberProxyProtocol {
|
||||
var userID: String {
|
||||
get { return underlyingUserID }
|
||||
|
||||
@@ -505,6 +505,23 @@ class SDKClientMock: SDKClientProtocol {
|
||||
restoreSessionSessionReceivedInvocations.append(session)
|
||||
try restoreSessionSessionClosure?(session)
|
||||
}
|
||||
//MARK: - roomDirectorySearch
|
||||
|
||||
public var roomDirectorySearchCallsCount = 0
|
||||
public var roomDirectorySearchCalled: Bool {
|
||||
return roomDirectorySearchCallsCount > 0
|
||||
}
|
||||
public var roomDirectorySearchReturnValue: RoomDirectorySearch!
|
||||
public var roomDirectorySearchClosure: (() -> RoomDirectorySearch)?
|
||||
|
||||
public func roomDirectorySearch() -> RoomDirectorySearch {
|
||||
roomDirectorySearchCallsCount += 1
|
||||
if let roomDirectorySearchClosure = roomDirectorySearchClosure {
|
||||
return roomDirectorySearchClosure()
|
||||
} else {
|
||||
return roomDirectorySearchReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - rooms
|
||||
|
||||
public var roomsCallsCount = 0
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
struct RoomDirectorySearchProxyMockConfiguration {
|
||||
let results: [RoomDirectorySearchResult]
|
||||
}
|
||||
|
||||
extension RoomDirectorySearchProxyMock {
|
||||
convenience init(configuration: RoomDirectorySearchProxyMockConfiguration) {
|
||||
self.init()
|
||||
resultsPublisher = CurrentValueSubject(configuration.results).asCurrentValuePublisher()
|
||||
searchQueryReturnValue = .success(())
|
||||
nextPageReturnValue = .success(())
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,7 @@ enum RoomAvatarSizeOnScreen {
|
||||
case globalSearch
|
||||
case details
|
||||
case notificationSettings
|
||||
case roomDirectorySearch
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
@@ -99,6 +100,8 @@ enum RoomAvatarSizeOnScreen {
|
||||
return 30
|
||||
case .timeline:
|
||||
return 32
|
||||
case .roomDirectorySearch:
|
||||
return 32
|
||||
case .messageForwarding:
|
||||
return 36
|
||||
case .globalSearch:
|
||||
|
||||
@@ -35,6 +35,7 @@ enum HomeScreenCoordinatorAction {
|
||||
case presentStartChatScreen
|
||||
case presentInvitesScreen
|
||||
case presentGlobalSearch
|
||||
case presentRoomDirectorySearch
|
||||
case logout
|
||||
}
|
||||
|
||||
@@ -83,6 +84,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentInvitesScreen)
|
||||
case .presentGlobalSearch:
|
||||
actionsSubject.send(.presentGlobalSearch)
|
||||
case .presentRoomDirectorySearch:
|
||||
actionsSubject.send(.presentRoomDirectorySearch)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -28,6 +28,7 @@ enum HomeScreenViewModelAction {
|
||||
case presentStartChatScreen
|
||||
case presentInvitesScreen
|
||||
case presentGlobalSearch
|
||||
case presentRoomDirectorySearch
|
||||
case logout
|
||||
}
|
||||
|
||||
@@ -46,6 +47,7 @@ enum HomeScreenViewAction {
|
||||
case markRoomAsUnread(roomIdentifier: String)
|
||||
case markRoomAsRead(roomIdentifier: String)
|
||||
case markRoomAsFavourite(roomIdentifier: String, isFavourite: Bool)
|
||||
case selectRoomDirectorySearch
|
||||
}
|
||||
|
||||
enum HomeScreenRoomListMode: CustomStringConvertible {
|
||||
@@ -88,6 +90,8 @@ struct HomeScreenViewState: BindableState {
|
||||
var hasPendingInvitations = false
|
||||
var hasUnreadPendingInvitations = false
|
||||
|
||||
var isRoomDirectorySearchEnabled = false
|
||||
|
||||
var selectedRoomID: String?
|
||||
|
||||
var visibleRooms: [HomeScreenRoom] {
|
||||
|
||||
@@ -94,6 +94,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
.sink { [weak self] _ in self?.updateRooms() }
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$publicSearchEnabled
|
||||
.weakAssign(to: \.state.isRoomDirectorySearchEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused)
|
||||
let searchQuery = context.$viewState.map(\.bindings.searchQuery)
|
||||
let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters)
|
||||
@@ -177,6 +181,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
Task {
|
||||
await markRoomAsFavourite(roomIdentifier, isFavourite: isFavourite)
|
||||
}
|
||||
case .selectRoomDirectorySearch:
|
||||
actionsSubject.send(.presentRoomDirectorySearch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ struct HomeScreenRoomList: View {
|
||||
// avoids glitches when focusing the search bar
|
||||
if !context.viewState.shouldHideRoomList {
|
||||
content
|
||||
} else if context.viewState.isRoomDirectorySearchEnabled {
|
||||
RoomDirectorySearchView {
|
||||
context.send(viewAction: .selectRoomDirectorySearch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDirectorySearchView: View {
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
Label(L10n.screenRoomlistRoomDirectoryButtonTitle, icon: \.listBulleted)
|
||||
}
|
||||
.buttonStyle(.compound(.secondary))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomDirectorySearchView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
RoomDirectorySearchView { }
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDirectorySearchScreenCoordinatorParameters {
|
||||
let roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol
|
||||
let imageProvider: ImageProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum RoomDirectorySearchScreenCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class RoomDirectorySearchScreenCoordinator: CoordinatorProtocol {
|
||||
private let viewModel: RoomDirectorySearchScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<RoomDirectorySearchScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<RoomDirectorySearchScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: RoomDirectorySearchScreenCoordinatorParameters) {
|
||||
viewModel = RoomDirectorySearchScreenViewModel(roomDirectorySearch: parameters.roomDirectorySearchProxy, userIndicatorController: parameters.userIndicatorController, imageProvider: parameters.imageProvider)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(RoomDirectorySearchScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum RoomDirectorySearchScreenViewModelAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct RoomDirectorySearchScreenViewState: BindableState {
|
||||
var searchResults: [RoomDirectorySearchResult] = []
|
||||
var isLoading = false
|
||||
|
||||
var bindings = RoomDirectorySearchScreenViewStateBindings()
|
||||
}
|
||||
|
||||
struct RoomDirectorySearchScreenViewStateBindings {
|
||||
var isSearching = false
|
||||
var searchString = ""
|
||||
}
|
||||
|
||||
enum RoomDirectorySearchScreenViewAction {
|
||||
case dismiss
|
||||
case join(roomID: String)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol RoomDirectorySearchScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<RoomDirectorySearchScreenViewModelAction, Never> { get }
|
||||
var context: RoomDirectorySearchScreenViewModelType.Context { get }
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias RoomDirectorySearchScreenViewModelType = StateStoreViewModel<RoomDirectorySearchScreenViewState, RoomDirectorySearchScreenViewAction>
|
||||
|
||||
class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType, RoomDirectorySearchScreenViewModelProtocol {
|
||||
private let roomDirectorySearch: RoomDirectorySearchProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<RoomDirectorySearchScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<RoomDirectorySearchScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(roomDirectorySearch: RoomDirectorySearchProxyProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
imageProvider: ImageProviderProtocol) {
|
||||
self.roomDirectorySearch = roomDirectorySearch
|
||||
self.userIndicatorController = userIndicatorController
|
||||
super.init(initialViewState: RoomDirectorySearchScreenViewState(), imageProvider: imageProvider)
|
||||
|
||||
roomDirectorySearch.resultsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.weakAssign(to: \.state.searchResults, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
context.$viewState.map(\.bindings.searchString)
|
||||
// only listen to the search string when is not loading
|
||||
.combineLatest(context.$viewState.map(\.isLoading)
|
||||
.filter { $0 == false })
|
||||
.map(\.0)
|
||||
.debounceTextQueriesAndRemoveDuplicates()
|
||||
.sink { [weak self] query in
|
||||
self?.search(query: query)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: RoomDirectorySearchScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .join(roomID: let roomID):
|
||||
joinRoom(roomID: roomID)
|
||||
}
|
||||
}
|
||||
|
||||
private func joinRoom(roomID: String) { }
|
||||
|
||||
private static let errorID = "roomDirectorySearchViewModelLoadingError"
|
||||
|
||||
private func search(query: String?) {
|
||||
guard !state.isLoading else {
|
||||
return
|
||||
}
|
||||
|
||||
state.isLoading = true
|
||||
Task {
|
||||
switch await roomDirectorySearch.search(query: query) {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.errorID,
|
||||
type: .toast,
|
||||
title: L10n.screenRoomDirectorySearchLoadingError,
|
||||
iconName: "xmark"))
|
||||
}
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDirectorySearchCell: View {
|
||||
let result: RoomDirectorySearchResult
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
|
||||
private var description: String? {
|
||||
if let topic = result.topic {
|
||||
return topic
|
||||
}
|
||||
|
||||
if result.name != nil, let alias = result.alias {
|
||||
return alias
|
||||
} else if result.name == nil, result.alias == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.id
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ListRow(label: .avatar(title: result.name ?? result.alias ?? result.id,
|
||||
description: description,
|
||||
icon: avatar), kind: .label)
|
||||
}
|
||||
|
||||
private var avatar: some View {
|
||||
LoadableAvatarImage(url: result.avatarURL,
|
||||
name: result.name,
|
||||
contentID: result.id,
|
||||
avatarSize: .room(on: .roomDirectorySearch),
|
||||
imageProvider: imageProvider)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
List {
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_1:matrix.org", alias: "#test:example.com", name: "Test title", topic: "test description", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_2:matrix.org", alias: "#test:example.com", name: nil, topic: "test description", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_3:example.com", alias: "#test_no_topic:example.com", name: "Test title no topic", topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_4:example.com", alias: "#test_no_topic:example.com", name: nil, topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_5:example.com", alias: nil, name: "Test title no alias", topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_6:example.com", alias: nil, name: "Test title no alias", topic: "Topic", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_7:example.com", alias: nil, name: nil, topic: "Topic", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_8:example.com", alias: nil, name: nil, topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDirectorySearchScreen: View {
|
||||
@ObservedObject var context: RoomDirectorySearchScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
ForEach(context.viewState.searchResults) {
|
||||
RoomDirectorySearchCell(result: $0, imageProvider: context.imageProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.environment(\.defaultMinListRowHeight, 48)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.isSearching($context.isSearching)
|
||||
.searchable(text: $context.searchString, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.navigationTitle(L10n.screenRoomDirectorySearchTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomDirectorySearchScreenScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomDirectorySearchScreenViewModel(roomDirectorySearch: RoomDirectorySearchProxyMock(configuration: .init(results: [.init(id: "test_1",
|
||||
alias: "#test_1:example.com",
|
||||
name: "Test 1",
|
||||
topic: "Test description 1",
|
||||
avatarURL: nil,
|
||||
canBeJoined: true),
|
||||
.init(id: "test_2",
|
||||
alias: "#test_2:example.com",
|
||||
name: "Test 2",
|
||||
topic: "Test description 2",
|
||||
avatarURL: URL.documentsDirectory,
|
||||
canBeJoined: false)])),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
imageProvider: MockMediaProvider())
|
||||
|
||||
static var previews: some View {
|
||||
RoomDirectorySearchScreen(context: viewModel.context)
|
||||
.snapshot(delay: 1.0)
|
||||
}
|
||||
}
|
||||
+1
@@ -50,6 +50,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
|
||||
var hideUnreadMessagesBadge: Bool { get set }
|
||||
var roomModerationEnabled: Bool { get set }
|
||||
var elementCallBaseURL: URL { get set }
|
||||
var publicSearchEnabled: Bool { get set }
|
||||
}
|
||||
|
||||
extension AppSettings: DeveloperOptionsProtocol { }
|
||||
|
||||
+6
@@ -47,6 +47,12 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Room Directory Search") {
|
||||
Toggle(isOn: $context.publicSearchEnabled) {
|
||||
Text("Public rooms search")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Element Call") {
|
||||
TextField(context.elementCallBaseURL.absoluteString, text: $elementCallBaseURLString)
|
||||
.submitLabel(.done)
|
||||
|
||||
@@ -541,6 +541,10 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func roomDirectorySearchProxy() -> RoomDirectorySearchProxyProtocol {
|
||||
RoomDirectorySearchProxy(roomDirectorySearch: client.roomDirectorySearch())
|
||||
}
|
||||
|
||||
// MARK: - Ignored users
|
||||
|
||||
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {
|
||||
|
||||
@@ -150,6 +150,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func profile(for userID: String) async -> Result<UserProfileProxy, ClientProxyError>
|
||||
|
||||
func roomDirectorySearchProxy() -> RoomDirectorySearchProxyProtocol
|
||||
|
||||
// MARK: - Ignored users
|
||||
|
||||
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
|
||||
private let roomDirectorySearch: RoomDirectorySearchProtocol
|
||||
private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomdirectorysearch", qos: .default)
|
||||
|
||||
private let resultsSubject = CurrentValueSubject<[RoomDirectorySearchResult], Never>([])
|
||||
|
||||
var resultsPublisher: CurrentValuePublisher<[RoomDirectorySearchResult], Never> {
|
||||
resultsSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private var results: [RoomDirectorySearchResult] {
|
||||
get { resultsSubject.value }
|
||||
set { resultsSubject.send(newValue) }
|
||||
}
|
||||
|
||||
private let diffsPublisher = PassthroughSubject<[RoomDirectorySearchEntryUpdate], Never>()
|
||||
|
||||
private var searchEntriesSubscription: TaskHandle?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(roomDirectorySearch: RoomDirectorySearchProtocol) {
|
||||
self.roomDirectorySearch = roomDirectorySearch
|
||||
diffsPublisher
|
||||
.receive(on: serialDispatchQueue)
|
||||
.sink { [weak self] in self?.updateResultsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
searchEntriesSubscription = await roomDirectorySearch.results(listener: RoomDirectorySearchEntriesListenerProxy { [weak self] updates in
|
||||
self?.diffsPublisher.send(updates)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func search(query: String?) async -> Result<Void, RoomDirectorySearchError> {
|
||||
do {
|
||||
try await roomDirectorySearch.search(filter: query, batchSize: 50)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func nextPage() async -> Result<Void, RoomDirectorySearchError> {
|
||||
do {
|
||||
try await roomDirectorySearch.nextPage()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.nextPageQueryFailed)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateResultsWithDiffs(_ updates: [RoomDirectorySearchEntryUpdate]) {
|
||||
results = updates.reduce(results) { currentItems, diff in
|
||||
processDiff(diff, on: currentItems)
|
||||
}
|
||||
}
|
||||
|
||||
private func processDiff(_ diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> [RoomDirectorySearchResult] {
|
||||
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
|
||||
return currentItems
|
||||
}
|
||||
|
||||
guard let updatedItems = currentItems.applying(collectionDiff) else {
|
||||
return currentItems
|
||||
}
|
||||
|
||||
return updatedItems
|
||||
}
|
||||
|
||||
private func buildDiff(from diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> CollectionDifference<RoomDirectorySearchResult>? {
|
||||
var changes = [CollectionDifference<RoomDirectorySearchResult>.Change]()
|
||||
|
||||
switch diff {
|
||||
case .append(let values):
|
||||
for (index, value) in values.enumerated() {
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: results.count + index, element: result, associatedWith: nil))
|
||||
}
|
||||
case .clear:
|
||||
for (index, value) in results.enumerated() {
|
||||
changes.append(.remove(offset: index, element: value, associatedWith: nil))
|
||||
}
|
||||
case .insert(let index, let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: Int(index), element: result, associatedWith: nil))
|
||||
case .popBack:
|
||||
guard let value = results.last else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
changes.append(.remove(offset: results.count - 1, element: value, associatedWith: nil))
|
||||
case .popFront:
|
||||
let result = results[0]
|
||||
changes.append(.remove(offset: 0, element: result, associatedWith: nil))
|
||||
case .pushBack(let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: results.count, element: result, associatedWith: nil))
|
||||
case .pushFront(let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: 0, element: result, associatedWith: nil))
|
||||
case .remove(let index):
|
||||
let result = results[Int(index)]
|
||||
changes.append(.remove(offset: Int(index), element: result, associatedWith: nil))
|
||||
case .reset(let values):
|
||||
for (index, result) in results.enumerated() {
|
||||
changes.append(.remove(offset: index, element: result, associatedWith: nil))
|
||||
}
|
||||
|
||||
for (index, value) in values.enumerated() {
|
||||
changes.append(.insert(offset: index, element: buildResultForRoomDescription(value), associatedWith: nil))
|
||||
}
|
||||
case .set(let index, let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.remove(offset: Int(index), element: result, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(index), element: result, associatedWith: nil))
|
||||
case .truncate(let length):
|
||||
for (index, value) in results.enumerated() {
|
||||
if index < length {
|
||||
continue
|
||||
}
|
||||
|
||||
changes.append(.remove(offset: index, element: value, associatedWith: nil))
|
||||
}
|
||||
}
|
||||
|
||||
return CollectionDifference(changes)
|
||||
}
|
||||
|
||||
private func buildResultForRoomDescription(_ value: RoomDescription) -> RoomDirectorySearchResult {
|
||||
RoomDirectorySearchResult(id: value.roomId,
|
||||
alias: value.alias,
|
||||
name: value.name,
|
||||
topic: value.topic,
|
||||
avatarURL: value.avatarUrl.flatMap(URL.init(string:)),
|
||||
canBeJoined: value.joinRule == .public)
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoomDirectorySearchEntriesListenerProxy: RoomDirectorySearchEntriesListener {
|
||||
private let onUpdateClosure: ([RoomDirectorySearchEntryUpdate]) -> Void
|
||||
|
||||
func onUpdate(roomEntriesUpdate: [RoomDirectorySearchEntryUpdate]) {
|
||||
onUpdateClosure(roomEntriesUpdate)
|
||||
}
|
||||
|
||||
init(_ onUpdateClosure: @escaping (([RoomDirectorySearchEntryUpdate]) -> Void)) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol RoomDirectorySearchProxyProtocol {
|
||||
var resultsPublisher: CurrentValuePublisher<[RoomDirectorySearchResult], Never> { get }
|
||||
|
||||
func search(query: String?) async -> Result<Void, RoomDirectorySearchError>
|
||||
func nextPage() async -> Result<Void, RoomDirectorySearchError>
|
||||
}
|
||||
|
||||
enum RoomDirectorySearchError: Error {
|
||||
case searchFailed
|
||||
case nextPageQueryFailed
|
||||
}
|
||||
|
||||
struct RoomDirectorySearchResult: Identifiable {
|
||||
let id: String
|
||||
let alias: String?
|
||||
let name: String?
|
||||
let topic: String?
|
||||
let avatarURL: URL?
|
||||
let canBeJoined: Bool
|
||||
}
|
||||
@@ -220,7 +220,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendAudio(url: url.path(percentEncoded: false), audioInfo: audioInfo, progressWatcher: UploadProgressListener { progress in
|
||||
let handle = timeline.sendAudio(url: url.path(percentEncoded: false), audioInfo: audioInfo, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
@@ -281,7 +281,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendImage(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), imageInfo: imageInfo, progressWatcher: UploadProgressListener { progress in
|
||||
let handle = timeline.sendImage(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), imageInfo: imageInfo, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
@@ -339,7 +339,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendVideo(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), videoInfo: videoInfo, progressWatcher: UploadProgressListener { progress in
|
||||
let handle = timeline.sendVideo(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), videoInfo: videoInfo, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
@@ -370,7 +370,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendVoiceMessage(url: url.path(percentEncoded: false), audioInfo: audioInfo, waveform: waveform, progressWatcher: UploadProgressListener { progress in
|
||||
let handle = timeline.sendVoiceMessage(url: url.path(percentEncoded: false), audioInfo: audioInfo, waveform: waveform, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user