mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 20:39:24 +00:00
Store and restore drafts (#2898)
This commit is contained in:
@@ -44,6 +44,7 @@ final class AppSettings {
|
||||
|
||||
// Feature flags
|
||||
case publicSearchEnabled
|
||||
case draftRestoringEnabled
|
||||
}
|
||||
|
||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
@@ -268,6 +269,9 @@ final class AppSettings {
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.publicSearchEnabled, defaultValue: isDevelopmentBuild, storageType: .volatile)
|
||||
var publicSearchEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.draftRestoringEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var draftRestoringEnabled
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -549,6 +549,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy)
|
||||
|
||||
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy,
|
||||
focussedEventID: focussedEventID,
|
||||
timelineController: timelineController,
|
||||
@@ -558,7 +560,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
emojiProvider: emojiProvider,
|
||||
completionSuggestionService: completionSuggestionService,
|
||||
appMediator: appMediator,
|
||||
appSettings: appSettings)
|
||||
appSettings: appSettings,
|
||||
composerDraftService: composerDraftService)
|
||||
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
coordinator.actions
|
||||
|
||||
@@ -4415,6 +4415,273 @@ class CompletionSuggestionServiceMock: CompletionSuggestionServiceProtocol {
|
||||
setSuggestionTriggerClosure?(suggestionTrigger)
|
||||
}
|
||||
}
|
||||
class ComposerDraftServiceMock: ComposerDraftServiceProtocol {
|
||||
|
||||
//MARK: - saveDraft
|
||||
|
||||
var saveDraftUnderlyingCallsCount = 0
|
||||
var saveDraftCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return saveDraftUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = saveDraftUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
saveDraftUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
saveDraftUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var saveDraftCalled: Bool {
|
||||
return saveDraftCallsCount > 0
|
||||
}
|
||||
var saveDraftReceivedDraft: ComposerDraftProxy?
|
||||
var saveDraftReceivedInvocations: [ComposerDraftProxy] = []
|
||||
|
||||
var saveDraftUnderlyingReturnValue: Result<Void, ComposerDraftServiceError>!
|
||||
var saveDraftReturnValue: Result<Void, ComposerDraftServiceError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return saveDraftUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, ComposerDraftServiceError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = saveDraftUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
saveDraftUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
saveDraftUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var saveDraftClosure: ((ComposerDraftProxy) async -> Result<Void, ComposerDraftServiceError>)?
|
||||
|
||||
func saveDraft(_ draft: ComposerDraftProxy) async -> Result<Void, ComposerDraftServiceError> {
|
||||
saveDraftCallsCount += 1
|
||||
saveDraftReceivedDraft = draft
|
||||
saveDraftReceivedInvocations.append(draft)
|
||||
if let saveDraftClosure = saveDraftClosure {
|
||||
return await saveDraftClosure(draft)
|
||||
} else {
|
||||
return saveDraftReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - loadDraft
|
||||
|
||||
var loadDraftUnderlyingCallsCount = 0
|
||||
var loadDraftCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loadDraftUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loadDraftUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loadDraftUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loadDraftUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var loadDraftCalled: Bool {
|
||||
return loadDraftCallsCount > 0
|
||||
}
|
||||
|
||||
var loadDraftUnderlyingReturnValue: Result<ComposerDraftProxy?, ComposerDraftServiceError>!
|
||||
var loadDraftReturnValue: Result<ComposerDraftProxy?, ComposerDraftServiceError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loadDraftUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<ComposerDraftProxy?, ComposerDraftServiceError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loadDraftUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loadDraftUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loadDraftUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var loadDraftClosure: (() async -> Result<ComposerDraftProxy?, ComposerDraftServiceError>)?
|
||||
|
||||
func loadDraft() async -> Result<ComposerDraftProxy?, ComposerDraftServiceError> {
|
||||
loadDraftCallsCount += 1
|
||||
if let loadDraftClosure = loadDraftClosure {
|
||||
return await loadDraftClosure()
|
||||
} else {
|
||||
return loadDraftReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - clearDraft
|
||||
|
||||
var clearDraftUnderlyingCallsCount = 0
|
||||
var clearDraftCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return clearDraftUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = clearDraftUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
clearDraftUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
clearDraftUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var clearDraftCalled: Bool {
|
||||
return clearDraftCallsCount > 0
|
||||
}
|
||||
|
||||
var clearDraftUnderlyingReturnValue: Result<Void, ComposerDraftServiceError>!
|
||||
var clearDraftReturnValue: Result<Void, ComposerDraftServiceError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return clearDraftUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, ComposerDraftServiceError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = clearDraftUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
clearDraftUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
clearDraftUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var clearDraftClosure: (() async -> Result<Void, ComposerDraftServiceError>)?
|
||||
|
||||
func clearDraft() async -> Result<Void, ComposerDraftServiceError> {
|
||||
clearDraftCallsCount += 1
|
||||
if let clearDraftClosure = clearDraftClosure {
|
||||
return await clearDraftClosure()
|
||||
} else {
|
||||
return clearDraftReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - getReply
|
||||
|
||||
var getReplyEventIDUnderlyingCallsCount = 0
|
||||
var getReplyEventIDCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return getReplyEventIDUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = getReplyEventIDUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
getReplyEventIDUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
getReplyEventIDUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var getReplyEventIDCalled: Bool {
|
||||
return getReplyEventIDCallsCount > 0
|
||||
}
|
||||
var getReplyEventIDReceivedEventID: String?
|
||||
var getReplyEventIDReceivedInvocations: [String] = []
|
||||
|
||||
var getReplyEventIDUnderlyingReturnValue: Result<TimelineItemReply, ComposerDraftServiceError>!
|
||||
var getReplyEventIDReturnValue: Result<TimelineItemReply, ComposerDraftServiceError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return getReplyEventIDUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<TimelineItemReply, ComposerDraftServiceError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = getReplyEventIDUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
getReplyEventIDUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
getReplyEventIDUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var getReplyEventIDClosure: ((String) async -> Result<TimelineItemReply, ComposerDraftServiceError>)?
|
||||
|
||||
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError> {
|
||||
getReplyEventIDCallsCount += 1
|
||||
getReplyEventIDReceivedEventID = eventID
|
||||
getReplyEventIDReceivedInvocations.append(eventID)
|
||||
if let getReplyEventIDClosure = getReplyEventIDClosure {
|
||||
return await getReplyEventIDClosure(eventID)
|
||||
} else {
|
||||
return getReplyEventIDReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class ElementCallServiceMock: ElementCallServiceProtocol {
|
||||
var actions: AnyPublisher<ElementCallServiceAction, Never> {
|
||||
get { return underlyingActions }
|
||||
@@ -10387,6 +10654,202 @@ class RoomProxyMock: RoomProxyProtocol {
|
||||
return matrixToEventPermalinkReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - saveDraft
|
||||
|
||||
var saveDraftUnderlyingCallsCount = 0
|
||||
var saveDraftCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return saveDraftUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = saveDraftUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
saveDraftUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
saveDraftUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var saveDraftCalled: Bool {
|
||||
return saveDraftCallsCount > 0
|
||||
}
|
||||
var saveDraftReceivedDraft: ComposerDraft?
|
||||
var saveDraftReceivedInvocations: [ComposerDraft] = []
|
||||
|
||||
var saveDraftUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var saveDraftReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return saveDraftUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = saveDraftUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
saveDraftUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
saveDraftUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var saveDraftClosure: ((ComposerDraft) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func saveDraft(_ draft: ComposerDraft) async -> Result<Void, RoomProxyError> {
|
||||
saveDraftCallsCount += 1
|
||||
saveDraftReceivedDraft = draft
|
||||
saveDraftReceivedInvocations.append(draft)
|
||||
if let saveDraftClosure = saveDraftClosure {
|
||||
return await saveDraftClosure(draft)
|
||||
} else {
|
||||
return saveDraftReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - loadDraft
|
||||
|
||||
var loadDraftUnderlyingCallsCount = 0
|
||||
var loadDraftCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loadDraftUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loadDraftUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loadDraftUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loadDraftUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var loadDraftCalled: Bool {
|
||||
return loadDraftCallsCount > 0
|
||||
}
|
||||
|
||||
var loadDraftUnderlyingReturnValue: Result<ComposerDraft?, RoomProxyError>!
|
||||
var loadDraftReturnValue: Result<ComposerDraft?, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loadDraftUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<ComposerDraft?, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loadDraftUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loadDraftUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loadDraftUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var loadDraftClosure: (() async -> Result<ComposerDraft?, RoomProxyError>)?
|
||||
|
||||
func loadDraft() async -> Result<ComposerDraft?, RoomProxyError> {
|
||||
loadDraftCallsCount += 1
|
||||
if let loadDraftClosure = loadDraftClosure {
|
||||
return await loadDraftClosure()
|
||||
} else {
|
||||
return loadDraftReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - clearDraft
|
||||
|
||||
var clearDraftUnderlyingCallsCount = 0
|
||||
var clearDraftCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return clearDraftUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = clearDraftUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
clearDraftUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
clearDraftUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var clearDraftCalled: Bool {
|
||||
return clearDraftCallsCount > 0
|
||||
}
|
||||
|
||||
var clearDraftUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var clearDraftReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return clearDraftUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = clearDraftUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
clearDraftUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
clearDraftUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var clearDraftClosure: (() async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func clearDraft() async -> Result<Void, RoomProxyError> {
|
||||
clearDraftCallsCount += 1
|
||||
if let clearDraftClosure = clearDraftClosure {
|
||||
return await clearDraftClosure()
|
||||
} else {
|
||||
return clearDraftReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class RoomSummaryProviderMock: RoomSummaryProviderProtocol {
|
||||
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
|
||||
@@ -12712,6 +13175,74 @@ class TimelineProxyMock: TimelineProxyProtocol {
|
||||
return sendPollResponsePollStartIDAnswersReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - getLoadedReplyDetails
|
||||
|
||||
var getLoadedReplyDetailsEventIDUnderlyingCallsCount = 0
|
||||
var getLoadedReplyDetailsEventIDCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return getLoadedReplyDetailsEventIDUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = getLoadedReplyDetailsEventIDUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
getLoadedReplyDetailsEventIDUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
getLoadedReplyDetailsEventIDUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var getLoadedReplyDetailsEventIDCalled: Bool {
|
||||
return getLoadedReplyDetailsEventIDCallsCount > 0
|
||||
}
|
||||
var getLoadedReplyDetailsEventIDReceivedEventID: String?
|
||||
var getLoadedReplyDetailsEventIDReceivedInvocations: [String] = []
|
||||
|
||||
var getLoadedReplyDetailsEventIDUnderlyingReturnValue: Result<InReplyToDetails, TimelineProxyError>!
|
||||
var getLoadedReplyDetailsEventIDReturnValue: Result<InReplyToDetails, TimelineProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return getLoadedReplyDetailsEventIDUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<InReplyToDetails, TimelineProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = getLoadedReplyDetailsEventIDUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
getLoadedReplyDetailsEventIDUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
getLoadedReplyDetailsEventIDUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var getLoadedReplyDetailsEventIDClosure: ((String) async -> Result<InReplyToDetails, TimelineProxyError>)?
|
||||
|
||||
func getLoadedReplyDetails(eventID: String) async -> Result<InReplyToDetails, TimelineProxyError> {
|
||||
getLoadedReplyDetailsEventIDCallsCount += 1
|
||||
getLoadedReplyDetailsEventIDReceivedEventID = eventID
|
||||
getLoadedReplyDetailsEventIDReceivedInvocations.append(eventID)
|
||||
if let getLoadedReplyDetailsEventIDClosure = getLoadedReplyDetailsEventIDClosure {
|
||||
return await getLoadedReplyDetailsEventIDClosure(eventID)
|
||||
} else {
|
||||
return getLoadedReplyDetailsEventIDReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class UserDiscoveryServiceMock: UserDiscoveryServiceProtocol {
|
||||
|
||||
|
||||
@@ -149,5 +149,7 @@ extension RoomProxyMock {
|
||||
|
||||
matrixToPermalinkReturnValue = .success(.homeDirectory)
|
||||
matrixToEventPermalinkReturnValue = .success(.homeDirectory)
|
||||
loadDraftReturnValue = .success(nil)
|
||||
clearDraftReturnValue = .success(())
|
||||
}
|
||||
}
|
||||
|
||||
+118
-4
@@ -27,6 +27,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
private let wysiwygViewModel: WysiwygComposerViewModel
|
||||
private let completionSuggestionService: CompletionSuggestionServiceProtocol
|
||||
private let analyticsService: AnalyticsService
|
||||
private let draftService: ComposerDraftServiceProtocol
|
||||
|
||||
private let mentionBuilder: MentionBuilderProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
@@ -46,15 +47,19 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
}
|
||||
|
||||
private var currentLinkData: WysiwygLinkData?
|
||||
|
||||
private var replyLoadingTask: Task<Void, Never>?
|
||||
|
||||
init(wysiwygViewModel: WysiwygComposerViewModel,
|
||||
completionSuggestionService: CompletionSuggestionServiceProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
mentionDisplayHelper: MentionDisplayHelper,
|
||||
analyticsService: AnalyticsService) {
|
||||
analyticsService: AnalyticsService,
|
||||
composerDraftService: ComposerDraftServiceProtocol) {
|
||||
self.wysiwygViewModel = wysiwygViewModel
|
||||
self.completionSuggestionService = completionSuggestionService
|
||||
self.analyticsService = analyticsService
|
||||
draftService = composerDraftService
|
||||
|
||||
mentionBuilder = MentionBuilder()
|
||||
attributedStringBuilder = AttributedStringBuilder(cacheKey: "Composer", mentionBuilder: mentionBuilder)
|
||||
@@ -177,6 +182,9 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
completionSuggestionService.processTextMessage(state.bindings.plainComposerText.string)
|
||||
case .didToggleFormattingOptions:
|
||||
if context.composerFormattingEnabled {
|
||||
guard !context.plainComposerText.string.isEmpty else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.wysiwygViewModel.textView.flushPills()
|
||||
self.wysiwygViewModel.setMarkdownContent(self.context.plainComposerText.string)
|
||||
@@ -191,13 +199,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
switch roomAction {
|
||||
case .setMode(mode: let mode):
|
||||
set(mode: mode)
|
||||
case .setText(text: let text):
|
||||
set(text: text)
|
||||
case .setText(let plainText, let htmlText):
|
||||
if let htmlText, context.composerFormattingEnabled {
|
||||
set(text: htmlText)
|
||||
} else {
|
||||
set(text: plainText)
|
||||
}
|
||||
case .removeFocus:
|
||||
state.bindings.composerFocused = false
|
||||
case .clear:
|
||||
set(mode: .default)
|
||||
set(text: "")
|
||||
case .saveDraft:
|
||||
handleSaveDraft()
|
||||
case .loadDraft:
|
||||
Task {
|
||||
await handleLoadDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +229,99 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func handleLoadDraft() async {
|
||||
guard case let .success(draft) = await draftService.loadDraft(),
|
||||
let draft else {
|
||||
return
|
||||
}
|
||||
|
||||
if let html = draft.htmlText {
|
||||
context.composerFormattingEnabled = true
|
||||
DispatchQueue.main.async {
|
||||
self.set(text: html)
|
||||
}
|
||||
} else {
|
||||
context.composerFormattingEnabled = false
|
||||
set(text: draft.plainText)
|
||||
}
|
||||
|
||||
switch draft.draftType {
|
||||
case .newMessage:
|
||||
set(mode: .default)
|
||||
case .edit(let eventID):
|
||||
set(mode: .edit(originalItemId: .init(timelineID: "", eventID: eventID)))
|
||||
case .reply(let eventID):
|
||||
set(mode: .reply(itemID: .init(timelineID: "", eventID: eventID), replyDetails: .loading(eventID: eventID), isThread: false))
|
||||
replyLoadingTask = Task {
|
||||
let reply = switch await draftService.getReply(eventID: eventID) {
|
||||
case .success(let reply):
|
||||
reply
|
||||
case .failure:
|
||||
TimelineItemReply(details: .error(eventID: eventID, message: L10n.commonSomethingWentWrong), isThreaded: false)
|
||||
}
|
||||
|
||||
guard !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
set(mode: .reply(itemID: .init(timelineID: "", eventID: eventID), replyDetails: reply.details, isThread: reply.isThreaded))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSaveDraft() {
|
||||
let plainText: String
|
||||
let htmlText: String?
|
||||
let type: ComposerDraftProxy.ComposerDraftType
|
||||
|
||||
if context.composerFormattingEnabled {
|
||||
if wysiwygViewModel.isContentEmpty, state.composerMode == .default {
|
||||
Task {
|
||||
await draftService.clearDraft()
|
||||
}
|
||||
return
|
||||
}
|
||||
plainText = wysiwygViewModel.content.markdown
|
||||
htmlText = wysiwygViewModel.content.html
|
||||
} else {
|
||||
if context.plainComposerText.string.isEmpty, state.composerMode == .default {
|
||||
Task {
|
||||
await draftService.clearDraft()
|
||||
}
|
||||
return
|
||||
}
|
||||
plainText = context.plainComposerText.string
|
||||
htmlText = nil
|
||||
}
|
||||
|
||||
switch state.composerMode {
|
||||
case .default:
|
||||
type = .newMessage
|
||||
case .edit(let itemID):
|
||||
guard let eventID = itemID.eventID else {
|
||||
MXLog.error("The event id for this message is missing")
|
||||
return
|
||||
}
|
||||
type = .edit(eventID: eventID)
|
||||
case .reply(let itemID, _, _):
|
||||
guard let eventID = itemID.eventID else {
|
||||
MXLog.error("The event id for this message is missing")
|
||||
return
|
||||
}
|
||||
type = .reply(eventID: eventID)
|
||||
default:
|
||||
// Do not save a draft for the other cases
|
||||
Task {
|
||||
await draftService.clearDraft()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
await draftService.saveDraft(.init(plainText: plainText, htmlText: htmlText, draftType: type))
|
||||
}
|
||||
}
|
||||
|
||||
private func sendPlainComposerText() {
|
||||
let attributedString = NSMutableAttributedString(attributedString: context.plainComposerText)
|
||||
|
||||
@@ -338,6 +449,10 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
}
|
||||
|
||||
private func set(mode: RoomScreenComposerMode) {
|
||||
if state.composerMode.isLoadingReply, state.composerMode.replyEventID != mode.replyEventID {
|
||||
replyLoadingTask?.cancel()
|
||||
}
|
||||
|
||||
guard mode != state.composerMode else { return }
|
||||
|
||||
state.composerMode = mode
|
||||
@@ -357,7 +472,6 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
private func set(text: String) {
|
||||
if context.composerFormattingEnabled {
|
||||
wysiwygViewModel.textView.flushPills()
|
||||
|
||||
wysiwygViewModel.setHtmlContent(text)
|
||||
} else {
|
||||
state.bindings.plainComposerText = .init(string: text)
|
||||
|
||||
@@ -292,7 +292,8 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init())),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory, range: .init()))]
|
||||
|
||||
@@ -315,6 +316,12 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||
ComposerToolbar.voiceMessagePreviewMock(uploading: false)
|
||||
}
|
||||
.previewDisplayName("Voice Message")
|
||||
|
||||
VStack(spacing: 8) {
|
||||
ComposerToolbar.replyLoadingPreviewMock(isLoading: true)
|
||||
ComposerToolbar.replyLoadingPreviewMock(isLoading: false)
|
||||
}
|
||||
.previewDisplayName("Reply")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +335,8 @@ extension ComposerToolbar {
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
model.state.composerEmpty = focused
|
||||
return model
|
||||
}
|
||||
@@ -344,7 +352,8 @@ extension ComposerToolbar {
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
model.state.composerEmpty = focused
|
||||
return model
|
||||
}
|
||||
@@ -360,7 +369,8 @@ extension ComposerToolbar {
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
model.state.composerMode = .recordVoiceMessage(state: AudioRecorderState())
|
||||
return model
|
||||
}
|
||||
@@ -377,7 +387,8 @@ extension ComposerToolbar {
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
model.state.composerMode = .previewVoiceMessage(state: AudioPlayerState(id: .recorderPreview, duration: 10.0), waveform: .data(waveformData), isUploading: uploading)
|
||||
return model
|
||||
}
|
||||
@@ -385,4 +396,27 @@ extension ComposerToolbar {
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
keyCommands: [])
|
||||
}
|
||||
|
||||
static func replyLoadingPreviewMock(isLoading: Bool) -> ComposerToolbar {
|
||||
let wysiwygViewModel = WysiwygComposerViewModel()
|
||||
var composerViewModel: ComposerToolbarViewModel {
|
||||
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
model.state.composerMode = isLoading ? .reply(itemID: .init(timelineID: ""),
|
||||
replyDetails: .loading(eventID: ""),
|
||||
isThread: false) :
|
||||
.reply(itemID: .init(timelineID: ""),
|
||||
replyDetails: .loaded(sender: .init(id: "",
|
||||
displayName: "Test"),
|
||||
eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false)
|
||||
return model
|
||||
}
|
||||
return ComposerToolbar(context: composerViewModel.context,
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
keyCommands: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
|
||||
static var previews: some View {
|
||||
RoomAttachmentPicker(context: viewModel.context)
|
||||
|
||||
@@ -30,6 +30,7 @@ struct RoomScreenCoordinatorParameters {
|
||||
let completionSuggestionService: CompletionSuggestionServiceProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
let appSettings: AppSettings
|
||||
let composerDraftService: ComposerDraftServiceProtocol
|
||||
}
|
||||
|
||||
enum RoomScreenCoordinatorAction {
|
||||
@@ -59,16 +60,17 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(parameters: RoomScreenCoordinatorParameters) {
|
||||
viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
|
||||
focussedEventID: parameters.focussedEventID,
|
||||
timelineController: parameters.timelineController,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
||||
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: parameters.appMediator,
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
let viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
|
||||
focussedEventID: parameters.focussedEventID,
|
||||
timelineController: parameters.timelineController,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
||||
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: parameters.appMediator,
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
self.viewModel = viewModel
|
||||
|
||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||
@@ -78,7 +80,13 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
completionSuggestionService: parameters.completionSuggestionService,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper(roomContext: viewModel.context),
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: parameters.composerDraftService)
|
||||
|
||||
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification).sink { _ in
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -128,6 +136,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel.process(composerAction: action)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Loading the draft requires the subscriptions to be set up first otherwise the room won't be be able to propagate the information to the composer.
|
||||
viewModel.loadDraft()
|
||||
}
|
||||
|
||||
func focusOnEvent(eventID: String) {
|
||||
@@ -135,6 +146,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
viewModel.saveDraft()
|
||||
viewModel.stop()
|
||||
}
|
||||
|
||||
@@ -143,7 +155,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
keyCommands: composerViewModel.keyCommands)
|
||||
|
||||
return AnyView(RoomScreen(context: viewModel.context, composerToolbar: composerToolbar))
|
||||
return AnyView(RoomScreen(context: viewModel.context, composerToolbar: composerToolbar)
|
||||
.onDisappear { [weak self] in
|
||||
self?.viewModel.saveDraft()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,16 +265,18 @@ class RoomScreenInteractionHandler {
|
||||
|
||||
private func processEditMessageEvent(_ messageTimelineItem: EventBasedMessageTimelineItemProtocol) {
|
||||
let text: String
|
||||
var htmlText: String?
|
||||
switch messageTimelineItem.contentType {
|
||||
case .text:
|
||||
text = messageTimelineItem.body
|
||||
case .emote:
|
||||
text = "/me " + messageTimelineItem.body
|
||||
case .text(let content):
|
||||
text = content.body
|
||||
htmlText = content.formattedBodyHTMLString
|
||||
case .emote(let content):
|
||||
text = "/me " + content.body
|
||||
default:
|
||||
text = messageTimelineItem.body
|
||||
}
|
||||
|
||||
actionsSubject.send(.composer(action: .setText(text: text)))
|
||||
actionsSubject.send(.composer(action: .setText(plainText: text, htmlText: htmlText)))
|
||||
actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id))))
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,29 @@ enum RoomScreenComposerMode: Equatable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isLoadingReply: Bool {
|
||||
switch self {
|
||||
case .reply(_, let replyDetails, _):
|
||||
switch replyDetails {
|
||||
case .loading:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var replyEventID: String? {
|
||||
switch self {
|
||||
case .reply(let itemID, _, _):
|
||||
return itemID.eventID
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomScreenViewPollAction {
|
||||
@@ -110,9 +133,11 @@ enum RoomScreenViewAction {
|
||||
|
||||
enum RoomScreenComposerAction {
|
||||
case setMode(mode: RoomScreenComposerMode)
|
||||
case setText(text: String)
|
||||
case setText(plainText: String, htmlText: String?)
|
||||
case removeFocus
|
||||
case clear
|
||||
case saveDraft
|
||||
case loadDraft
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
|
||||
@@ -133,11 +133,22 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func loadDraft() {
|
||||
guard appSettings.draftRestoringEnabled else {
|
||||
return
|
||||
}
|
||||
actionsSubject.send(.composer(action: .loadDraft))
|
||||
}
|
||||
|
||||
func stop() {
|
||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
||||
state.bindings.mediaPreviewItem = nil
|
||||
}
|
||||
|
||||
func saveDraft() {
|
||||
actionsSubject.send(.composer(action: .saveDraft))
|
||||
}
|
||||
|
||||
override func process(viewAction: RoomScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .itemAppeared(let id):
|
||||
|
||||
@@ -26,4 +26,6 @@ protocol RoomScreenViewModelProtocol {
|
||||
/// Updates the timeline to show and highlight the item with the corresponding event ID.
|
||||
func focusOnEvent(eventID: String) async
|
||||
func stop()
|
||||
func loadDraft()
|
||||
func saveDraft()
|
||||
}
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ enum DeveloperOptionsScreenViewAction {
|
||||
protocol DeveloperOptionsProtocol: AnyObject {
|
||||
var logLevel: TracingConfiguration.LogLevel { get set }
|
||||
var hideUnreadMessagesBadge: Bool { get set }
|
||||
var draftRestoringEnabled: Bool { get set }
|
||||
var elementCallBaseURL: URL { get set }
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -32,6 +32,12 @@ struct DeveloperOptionsScreen: View {
|
||||
Text("Hide grey dots")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Room") {
|
||||
Toggle(isOn: $context.draftRestoringEnabled) {
|
||||
Text("Allow drafts to be restored")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Element Call") {
|
||||
TextField(context.elementCallBaseURL.absoluteString, text: $elementCallBaseURLString)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
final class ComposerDraftService: ComposerDraftServiceProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let timelineItemfactory: RoomTimelineItemFactoryProtocol
|
||||
|
||||
init(roomProxy: RoomProxyProtocol, timelineItemfactory: RoomTimelineItemFactoryProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
self.timelineItemfactory = timelineItemfactory
|
||||
}
|
||||
|
||||
func saveDraft(_ draft: ComposerDraftProxy) async -> Result<Void, ComposerDraftServiceError> {
|
||||
switch await roomProxy.saveDraft(draft.toRust) {
|
||||
case .success:
|
||||
MXLog.info("Successfully saved draft")
|
||||
return .success(())
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed to save draft: \(error)")
|
||||
return .failure(.failedToSaveDraft)
|
||||
}
|
||||
}
|
||||
|
||||
func loadDraft() async -> Result<ComposerDraftProxy?, ComposerDraftServiceError> {
|
||||
switch await roomProxy.loadDraft() {
|
||||
case .success(let draft):
|
||||
guard let draft else {
|
||||
return .success(nil)
|
||||
}
|
||||
return .success(ComposerDraftProxy(from: draft))
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed to load draft: \(error)")
|
||||
return .failure(.failedToLoadDraft)
|
||||
}
|
||||
}
|
||||
|
||||
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError> {
|
||||
switch await roomProxy.timeline.getLoadedReplyDetails(eventID: eventID) {
|
||||
case .success(let replyDetails):
|
||||
return await .success(timelineItemfactory.buildReply(details: replyDetails))
|
||||
case .failure(let error):
|
||||
MXLog.error("Could not load reply: \(error)")
|
||||
return .failure(.failedToLoadReply)
|
||||
}
|
||||
}
|
||||
|
||||
func clearDraft() async -> Result<Void, ComposerDraftServiceError> {
|
||||
switch await roomProxy.clearDraft() {
|
||||
case .success:
|
||||
MXLog.info("Successfully cleared draft")
|
||||
return .success(())
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed to clear draft: \(error)")
|
||||
return .failure(.failedToClearDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
struct ComposerDraftProxy: Equatable {
|
||||
enum ComposerDraftType: Equatable {
|
||||
case newMessage
|
||||
case reply(eventID: String)
|
||||
case edit(eventID: String)
|
||||
|
||||
var toRust: MatrixRustSDK.ComposerDraftType {
|
||||
switch self {
|
||||
case .newMessage:
|
||||
return .newMessage
|
||||
case .edit(let eventID):
|
||||
return .edit(eventId: eventID)
|
||||
case .reply(let eventID):
|
||||
return .reply(eventId: eventID)
|
||||
}
|
||||
}
|
||||
|
||||
init(from rustDraftType: MatrixRustSDK.ComposerDraftType) {
|
||||
switch rustDraftType {
|
||||
case .newMessage:
|
||||
self = .newMessage
|
||||
case .edit(let eventID):
|
||||
self = .edit(eventID: eventID)
|
||||
case .reply(let eventID):
|
||||
self = .reply(eventID: eventID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let plainText: String
|
||||
let htmlText: String?
|
||||
let draftType: ComposerDraftType
|
||||
|
||||
var toRust: ComposerDraft {
|
||||
ComposerDraft(plainText: plainText, htmlText: htmlText, draftType: draftType.toRust)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposerDraftProxy {
|
||||
init(from rustDraft: ComposerDraft) {
|
||||
plainText = rustDraft.plainText
|
||||
htmlText = rustDraft.htmlText
|
||||
draftType = ComposerDraftType(from: rustDraft.draftType)
|
||||
}
|
||||
}
|
||||
|
||||
enum ComposerDraftServiceError: Error {
|
||||
case failedToLoadDraft
|
||||
case failedToLoadReply
|
||||
case failedToSaveDraft
|
||||
case failedToClearDraft
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol ComposerDraftServiceProtocol {
|
||||
func saveDraft(_ draft: ComposerDraftProxy) async -> Result<Void, ComposerDraftServiceError>
|
||||
func loadDraft() async -> Result<ComposerDraftProxy?, ComposerDraftServiceError>
|
||||
func clearDraft() async -> Result<Void, ComposerDraftServiceError>
|
||||
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError>
|
||||
}
|
||||
@@ -565,6 +565,37 @@ class RoomProxy: RoomProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Drafts
|
||||
|
||||
func saveDraft(_ draft: ComposerDraft) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.saveComposerDraft(draft: draft)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed saving draft with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func loadDraft() async -> Result<ComposerDraft?, RoomProxyError> {
|
||||
do {
|
||||
return try await .success(room.loadComposerDraft())
|
||||
} catch {
|
||||
MXLog.error("Failed restoring draft with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func clearDraft() async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.clearComposerDraft()
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed clearing draft with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
@@ -126,7 +126,6 @@ protocol RoomProxyProtocol {
|
||||
// MARK: - Element Call
|
||||
|
||||
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError>
|
||||
|
||||
func elementCallWidgetDriver() -> ElementCallWidgetDriverProtocol
|
||||
|
||||
func sendCallNotificationIfNeeeded() async -> Result<Void, RoomProxyError>
|
||||
@@ -134,8 +133,13 @@ protocol RoomProxyProtocol {
|
||||
// MARK: - Permalinks
|
||||
|
||||
func matrixToPermalink() async -> Result<URL, RoomProxyError>
|
||||
|
||||
func matrixToEventPermalink(_ eventID: String) async -> Result<URL, RoomProxyError>
|
||||
|
||||
// MARK: - Drafts
|
||||
|
||||
func saveDraft(_ draft: ComposerDraft) async -> Result<Void, RoomProxyError>
|
||||
func loadDraft() async -> Result<ComposerDraft?, RoomProxyError>
|
||||
func clearDraft() async -> Result<Void, RoomProxyError>
|
||||
}
|
||||
|
||||
extension RoomProxyProtocol {
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TimelineItemReply {
|
||||
let details: TimelineItemReplyDetails
|
||||
let isThreaded: Bool
|
||||
}
|
||||
|
||||
enum TimelineItemReplyDetails: Hashable {
|
||||
case notLoaded(eventID: String)
|
||||
case loading(eventID: String)
|
||||
|
||||
@@ -216,7 +216,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildTextTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -236,7 +236,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildImageTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -256,7 +256,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildVideoTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -276,7 +276,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildAudioTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -296,7 +296,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildAudioTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -316,7 +316,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildFileTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -336,7 +336,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildNoticeTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -356,7 +356,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -376,7 +376,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildLocationTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -645,14 +645,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
// MARK: - Reply details
|
||||
|
||||
private func buildReplyToDetailsFrom(details: InReplyToDetails?) -> TimelineItemReplyDetails? {
|
||||
guard let details else { return nil }
|
||||
|
||||
func buildReply(details: InReplyToDetails) -> TimelineItemReply {
|
||||
let isThreaded = details.event.isThreaded
|
||||
switch details.event {
|
||||
case .unavailable:
|
||||
return .notLoaded(eventID: details.eventId)
|
||||
return .init(details: .notLoaded(eventID: details.eventId), isThreaded: isThreaded)
|
||||
case .pending:
|
||||
return .loading(eventID: details.eventId)
|
||||
return .init(details: .loading(eventID: details.eventId), isThreaded: isThreaded)
|
||||
case let .ready(timelineItem, senderID, senderProfile):
|
||||
let sender: TimelineItemSender
|
||||
switch senderProfile {
|
||||
@@ -672,7 +671,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
switch timelineItem.kind() {
|
||||
case .message:
|
||||
return timelineItemReplyDetails(sender: sender, eventID: details.eventId, messageType: timelineItem.asMessage()?.msgtype())
|
||||
return .init(details: timelineItemReplyDetails(sender: sender, eventID: details.eventId, messageType: timelineItem.asMessage()?.msgtype()), isThreaded: isThreaded)
|
||||
case .poll(let question, _, _, _, _, _, _):
|
||||
replyContent = .poll(question: question)
|
||||
case .sticker(let body, _, _):
|
||||
@@ -683,12 +682,20 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||
}
|
||||
|
||||
return .loaded(sender: sender, eventID: details.eventId, eventContent: replyContent)
|
||||
return .init(details: .loaded(sender: sender, eventID: details.eventId, eventContent: replyContent), isThreaded: isThreaded)
|
||||
case let .error(message):
|
||||
return .error(eventID: details.eventId, message: message)
|
||||
return .init(details: .error(eventID: details.eventId, message: message), isThreaded: isThreaded)
|
||||
}
|
||||
}
|
||||
|
||||
private func buildReplyToDetailsFromDetailsIfAvailable(details: InReplyToDetails?) -> TimelineItemReplyDetails? {
|
||||
guard let details else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return buildReply(details: details).details
|
||||
}
|
||||
|
||||
private func timelineItemReplyDetails(sender: TimelineItemSender, eventID: String, messageType: MessageType?) -> TimelineItemReplyDetails {
|
||||
let replyContent: EventBasedMessageTimelineItemContentType
|
||||
|
||||
@@ -733,3 +740,14 @@ extension Poll.Kind {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension RepliedToEventDetails {
|
||||
var isThreaded: Bool {
|
||||
switch self {
|
||||
case .ready(let content, _, _):
|
||||
return content.asMessage()?.isThreaded() ?? false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineItemFactoryProtocol {
|
||||
func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol?
|
||||
func buildReply(details: InReplyToDetails) -> TimelineItemReply
|
||||
}
|
||||
|
||||
@@ -172,6 +172,15 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func getLoadedReplyDetails(eventID: String) async -> Result<InReplyToDetails, TimelineProxyError> {
|
||||
do {
|
||||
return try await .success(timeline.loadReplyDetails(eventIdStr: eventID))
|
||||
} catch {
|
||||
MXLog.error("Failed getting reply details for event \(eventID) with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sending
|
||||
|
||||
func sendAudio(url: URL,
|
||||
|
||||
@@ -107,6 +107,8 @@ protocol TimelineProxyProtocol {
|
||||
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func getLoadedReplyDetails(eventID: String) async -> Result<InReplyToDetails, TimelineProxyError>
|
||||
}
|
||||
|
||||
extension TimelineProxyProtocol {
|
||||
|
||||
@@ -256,7 +256,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -272,7 +273,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -288,7 +290,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -304,7 +307,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -323,7 +327,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -342,7 +347,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -361,7 +367,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -381,7 +388,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -400,7 +408,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -418,7 +427,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
@@ -450,7 +460,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -469,7 +480,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -488,7 +500,8 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
Reference in New Issue
Block a user