mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Sliding Sync + New Timeline API (#189)
* Begin adopting new Timeline API. * Add edited indicator and reactions. * vector-im/element-x-ios/issues/65 - Sliding sync support * Fix missing room display name, wrong placeholder avatar text color and various other warnings that would fail the build on the CI * Various tweaks: * using release version of the demo branch of the sdk * enabled home screen last room messages * switched debug mode rust logging to warn * enabled redactions * enabled new logout flows and soft logout * enabled replies * Fix room member display name and avatar crashes / race condition, fix unit tests * Make the ClientProxy and the UserSession MainActors * Remove unused MatrixRustSDK imports, we should strive to keep these only in top level services and proxies * Don't start either of the syncs while in soft logout * #181: Style the session verification banner to match Figma. * #181: Update verification modal. * #181: Update snapshot tests. * Make session verification state machine less pedantic * Remove unnecessary weak selfs * Various tweaks following code review: * add start and stop sync client proxy methods * move ss proxy url the build settings * made media provider load results discardable * added publishers for the roomSummaryProvider's total number of rooms and state * Fix when sender details are shown * Disable sync v2, causes duplicates in the timeline (as expected) * Move ClientProxy media loading off the main queue and into a detached task * Another attempt at moving image loading off the main queue * Moved home screen diffing and latest room fetching to the background * Prevent the timeline composer from becoming the first responder when not needed * Bump to a newer version of the RustSDK * Fixes vector-im/element-x-ios/issues/107 - New home screen design * Implement thumbnail loading instead of full image avatars. * Revert "Disable sync v2, causes duplicates in the timeline (as expected)" * Add support for local echoes, dispatching detached tasks to a concurrenc GCD queue * Move the session verification banner to a List Section to avoid UI glitches * Optimise room mapping after sliding sync updates and thumbnail fetching * Replace home screen List with a LazyVStack in an attempt to fix performance. Moved move summary provider room updating to a background thread * Fixes vector-im/element-x-ios/issues/177 - New Bubbles Design * Define in group state for timeline items * Add replies into the bubble * Add timeline width environment value * Add `RoundedCorner` shape with specific corners rounding * Add in group state for previews * Implement bubble grouping logic * Timeline avatar layout changes * Fix placeholder avatars for dark mode * New bubbles design * Update mock timeline items * Update timeline separator design * Update room screen reference screenshots * Add changelog * Formatting fixes * Add some space before single or beginning outgoing items * Redesign the message composer * Handle the msgtype enum. * Update room name label line limit and incoming bubble background. Disabled syncv2, ss withCommonExtensions and session verification controller checking * Increase default back pagination limit. * Stop parsing links and tidy up composer button. * Also fix the frame of an image whilst loading. * Bump SDK package version. * Remove app states about settings * Add strings * Use colors on placeholder avatars * Tiny changes for placeholder avatars * Update settings screen design * Provide a user display name from the mock client * Settings screen presentation logic * Add changelog * Update reference screenshots Co-authored-by: Doug <douglase@element.io> Co-authored-by: ismailgulek <ismailgulek@users.noreply.github.com> Co-authored-by: ismailgulek <ismailg@matrix.org>
This commit is contained in:
parent
a88491be1a
commit
5ebe923991
@ -34,12 +34,14 @@ public struct ElementActionButtonStyle: ButtonStyle {
|
||||
public var size: ElementControlSize
|
||||
public var color: Color
|
||||
|
||||
private var verticalPadding: CGFloat { size == .xLarge ? 12 : 4 }
|
||||
private var cornerRadius: CGFloat { size == .xLarge ? 14 : 8 }
|
||||
private var verticalPadding: CGFloat { size == .xLarge ? 14 : 4 }
|
||||
private var maxWidth: CGFloat? { size == .xLarge ? .infinity : nil }
|
||||
|
||||
private var fontColor: Color {
|
||||
// Always white unless disabled with a dark theme.
|
||||
.white.opacity(colorScheme == .dark && !isEnabled ? 0.3 : 1.0)
|
||||
Color.element.systemPrimaryBackground
|
||||
.opacity(colorScheme == .dark && !isEnabled ? 0.3 : 1.0)
|
||||
}
|
||||
|
||||
public init(size: ElementControlSize = .regular, color: Color = .element.accent) {
|
||||
@ -53,9 +55,9 @@ public struct ElementActionButtonStyle: ButtonStyle {
|
||||
.padding(.vertical, verticalPadding)
|
||||
.frame(maxWidth: maxWidth)
|
||||
.foregroundColor(fontColor)
|
||||
.font(.element.body)
|
||||
.font(.element.bodyBold)
|
||||
.background(color.opacity(backgroundOpacity(when: configuration.isPressed)))
|
||||
.cornerRadius(8.0)
|
||||
.cornerRadius(cornerRadius)
|
||||
}
|
||||
|
||||
private func backgroundOpacity(when isPressed: Bool) -> CGFloat {
|
||||
|
87
DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift
Normal file
87
DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
public extension ButtonStyle where Self == ElementCapsuleButtonStyle {
|
||||
/// A button style that uses a capsule shape with a regular appearance.
|
||||
static var elementCapsule: ElementCapsuleButtonStyle {
|
||||
ElementCapsuleButtonStyle(isProminent: false)
|
||||
}
|
||||
|
||||
/// A button style that uses a capsule shape with a prominent appearance.
|
||||
static var elementCapsuleProminent: ElementCapsuleButtonStyle {
|
||||
ElementCapsuleButtonStyle(isProminent: true)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ElementCapsuleButtonStyle: ButtonStyle {
|
||||
let isProminent: Bool
|
||||
|
||||
public func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.padding(7)
|
||||
.frame(maxWidth: .infinity)
|
||||
.font(.element.footnote)
|
||||
.foregroundColor(fontColor)
|
||||
.multilineTextAlignment(.center)
|
||||
.background(background)
|
||||
.opacity(configuration.isPressed ? 0.6 : 1)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var background: some View {
|
||||
if isProminent {
|
||||
Capsule()
|
||||
.foregroundColor(Color.element.accent)
|
||||
} else {
|
||||
Capsule()
|
||||
.stroke(Color.element.accent)
|
||||
}
|
||||
}
|
||||
|
||||
var fontColor: Color {
|
||||
isProminent ? .element.systemPrimaryBackground : .element.systemPrimaryLabel
|
||||
}
|
||||
}
|
||||
|
||||
struct ElementCapsuleButtonStyle_Previews: PreviewProvider {
|
||||
public static var states: some View {
|
||||
VStack {
|
||||
Button("Enabled") { /* preview */ }
|
||||
.buttonStyle(.elementCapsuleProminent)
|
||||
|
||||
Button("Disabled") { /* preview */ }
|
||||
.buttonStyle(.elementCapsuleProminent)
|
||||
.disabled(true)
|
||||
|
||||
Button("Enabled") { /* preview */ }
|
||||
.buttonStyle(.elementCapsule)
|
||||
|
||||
Button("Disabled") { /* preview */ }
|
||||
.buttonStyle(.elementCapsule)
|
||||
.disabled(true)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
public static var previews: some View {
|
||||
states
|
||||
.preferredColorScheme(.light)
|
||||
states
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@
|
||||
01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */; };
|
||||
01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109C0201D8CB3F947340DC80 /* WeakDictionary.swift */; };
|
||||
02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; };
|
||||
03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */; };
|
||||
03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */; };
|
||||
03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26747B3154A5DBC3A7E24A5 /* Image.swift */; };
|
||||
04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422724361B6555364C43281E /* RoomHeaderView.swift */; };
|
||||
@ -27,6 +26,7 @@
|
||||
07240B7159A3990C4C2E8FFC /* LoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */; };
|
||||
072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; };
|
||||
09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; };
|
||||
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; };
|
||||
0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; };
|
||||
0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */; };
|
||||
0E8C480700870BB34A2A360F /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4346F63D53A346271577FD9C /* AppAuth */; };
|
||||
@ -53,16 +53,15 @@
|
||||
191161FE9E0DA89704301F37 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; };
|
||||
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; };
|
||||
19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */; };
|
||||
1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; };
|
||||
1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; };
|
||||
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; };
|
||||
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; };
|
||||
1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; };
|
||||
206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; };
|
||||
214C6B416609E58CCBF6DCEE /* SoftLogoutModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */; };
|
||||
224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */; };
|
||||
2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */; };
|
||||
22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */; };
|
||||
2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */; };
|
||||
237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; };
|
||||
23B2CD5A06B16055BDDD0994 /* ApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */; };
|
||||
24906A1E82D0046655958536 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CF12478983A5EB390FB26 /* MessageComposer.swift */; };
|
||||
@ -72,13 +71,11 @@
|
||||
28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */; };
|
||||
290FDB0FFDC2F1DDF660343E /* TestMeasurementParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */; };
|
||||
297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */; };
|
||||
29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BDAC8895AB2B77B47703AE /* MockRoomSummary.swift */; };
|
||||
29EE1791E0AFA1ABB7F23D2F /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = A981A4CA233FB5C13B9CA690 /* SwiftyBeaver */; };
|
||||
2A90D9F91A836E30B7D78838 /* MXLogObjcWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E438DBCBDC7A41B95DDDD9 /* MXLogObjcWrapper.m */; };
|
||||
2BA59D0AEFB4B82A2EC2A326 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; };
|
||||
2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; };
|
||||
2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90733775209F4D4D366A268F /* RootRouterType.swift */; };
|
||||
2E59008365E01F0AFB3A6B24 /* ImageRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */; };
|
||||
2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; };
|
||||
2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */; };
|
||||
2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; };
|
||||
@ -87,7 +84,6 @@
|
||||
308BD9343B95657FAA583FB7 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AD2AC190E55B2BD4D0F1D4A7 /* SwiftyBeaver */; };
|
||||
3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; };
|
||||
313382FC5D38064EAAA35CB2 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D1CC633517D695FEC54208 /* FileManager.swift */; };
|
||||
33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; };
|
||||
33CAC1226DFB8B5D8447D286 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; };
|
||||
344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; };
|
||||
34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; };
|
||||
@ -105,15 +101,14 @@
|
||||
3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; };
|
||||
3C549A0BF39F8A854D45D9FD /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
|
||||
3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; };
|
||||
3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4411C0DA0087A1CB143E96FA /* EventBrief.swift */; };
|
||||
3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; };
|
||||
3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; };
|
||||
3F2148F11164C7C5609984EB /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; };
|
||||
407DCE030E0F9B7C9861D38A /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; };
|
||||
418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */; };
|
||||
41DFDD212D1BE57CA50D783B /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; };
|
||||
438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; };
|
||||
43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; };
|
||||
447E8580A0A2569E32529E17 /* MockRoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */; };
|
||||
462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; };
|
||||
46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; };
|
||||
4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */; };
|
||||
@ -154,6 +149,7 @@
|
||||
62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210612D17A39369480FC183 /* MediaSource.swift */; };
|
||||
63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; };
|
||||
64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; };
|
||||
663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; };
|
||||
6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; };
|
||||
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; };
|
||||
67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; };
|
||||
@ -228,6 +224,7 @@
|
||||
9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.swift */; };
|
||||
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; };
|
||||
97CECF91D68235F1D13598D7 /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; };
|
||||
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; };
|
||||
989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; };
|
||||
992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; };
|
||||
99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; };
|
||||
@ -245,19 +242,20 @@
|
||||
9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; };
|
||||
A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */; };
|
||||
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; };
|
||||
A0A26E4F713596856470EF1A /* RoomTimelineProviderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABAECB0CA5FF8F8E6F10DD7 /* RoomTimelineProviderItem.swift */; };
|
||||
A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; };
|
||||
A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FC861755C6388F62B9280A /* Analytics.swift */; };
|
||||
A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; };
|
||||
A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; };
|
||||
A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; };
|
||||
A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; };
|
||||
A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */; };
|
||||
A636D4900E0D98ED91536482 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3EDF23226895776553F04A /* AppCoordinator.swift */; };
|
||||
A663FE6704CB500EBE782AE1 /* AnalyticsPromptCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DE1CF8F5EFD353B1A5E36F /* AnalyticsPromptCoordinator.swift */; };
|
||||
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
|
||||
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
|
||||
A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; };
|
||||
A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; };
|
||||
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; };
|
||||
AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; };
|
||||
AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; };
|
||||
AB4C5D62A21AD712811CE8CD /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68232D336E2B546AD95B78B5 /* XCUIElement.swift */; };
|
||||
@ -281,11 +279,11 @@
|
||||
BB6B0B91CE11E06330017000 /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */; };
|
||||
BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EF188681D6B6068CFAEAFC3F /* MXLogger.m */; };
|
||||
BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */; };
|
||||
BE3237142FA6E1A13C0E7D11 /* RoomSummaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */; };
|
||||
BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */; };
|
||||
BF35062D06888FA80BD139FF /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB7F9D6FC121204D59E18DF /* Presentable.swift */; };
|
||||
C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */; };
|
||||
C2CF93B067FD935E4F82FE44 /* SplashScreenPageIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850064FF8D7DB9C875E7AA1A /* SplashScreenPageIndicator.swift */; };
|
||||
C35CF4DAB1467FE1BBDC204B /* MessageTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAF1C75771D9DC75877F4B4 /* MessageTimelineItem.swift */; };
|
||||
C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; };
|
||||
C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; };
|
||||
C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; };
|
||||
@ -303,7 +301,6 @@
|
||||
CE7A715947ABAB1DEB5C21D7 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */; };
|
||||
CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; };
|
||||
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; };
|
||||
D013E70C8E28E43497820444 /* TextRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */; };
|
||||
D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */; };
|
||||
D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; };
|
||||
D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; };
|
||||
@ -322,6 +319,7 @@
|
||||
E01373F2043E76393A0CE073 /* AnalyticsPromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11B74ACE8D71747E1044A9C /* AnalyticsPromptViewModel.swift */; };
|
||||
E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; };
|
||||
E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; };
|
||||
E290C78E7F09F47FD2662986 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; };
|
||||
E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; };
|
||||
E47CD939D8480657D4B706C6 /* AnalyticsPromptCheckmarkItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7B2E9CC5DC3B76ADC35A43 /* AnalyticsPromptCheckmarkItem.swift */; };
|
||||
E481C8FDCB6C089963C95344 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; };
|
||||
@ -346,7 +344,7 @@
|
||||
F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; };
|
||||
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
|
||||
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; };
|
||||
F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */; };
|
||||
F764BE976EAB76D63E7C1678 /* RoundedCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8023C7413A426FBF0A52B684 /* RoundedCorner.swift */; };
|
||||
F99FB21EFC6D99D247FE7CBE /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = D82E84F90358CC1118E6034B /* Introspect */; };
|
||||
FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; };
|
||||
FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */; };
|
||||
@ -394,7 +392,6 @@
|
||||
0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
||||
095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = "<group>"; };
|
||||
09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
0AB7A0C06CB527A1095DEB33 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = da; path = da.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = "<group>"; };
|
||||
@ -412,6 +409,7 @@
|
||||
105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = "<group>"; };
|
||||
105D16E7DB0CCE9526612BDD /* bn-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-IN"; path = "bn-IN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
109C0201D8CB3F947340DC80 /* WeakDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionary.swift; sourceTree = "<group>"; };
|
||||
10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = "<group>"; };
|
||||
113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@ -470,7 +468,6 @@
|
||||
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = "<group>"; };
|
||||
35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactory.swift; sourceTree = "<group>"; };
|
||||
3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3782C506F4FF1AADF61B6212 /* tlh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tlh; path = tlh.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = "<group>"; };
|
||||
@ -492,11 +489,9 @@
|
||||
3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = "<group>"; };
|
||||
3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; };
|
||||
40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomMessage.swift; sourceTree = "<group>"; };
|
||||
4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenLoadingViewPresenter.swift; sourceTree = "<group>"; };
|
||||
422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = "<group>"; };
|
||||
434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
4411C0DA0087A1CB143E96FA /* EventBrief.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBrief.swift; sourceTree = "<group>"; };
|
||||
4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewPresenter.swift; sourceTree = "<group>"; };
|
||||
4488F5F92A64A137665C96CD /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
44AEEE13AC1BF303AE48CBF8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@ -572,7 +567,6 @@
|
||||
68232D336E2B546AD95B78B5 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = "<group>"; };
|
||||
68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = "<group>"; };
|
||||
6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; };
|
||||
6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProvider.swift; sourceTree = "<group>"; };
|
||||
6A1AAC8EB2992918D01874AC /* rue */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = rue; path = rue.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProperties+Element.swift"; sourceTree = "<group>"; };
|
||||
6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = "<group>"; };
|
||||
@ -607,6 +601,7 @@
|
||||
7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = "<group>"; };
|
||||
7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = "<group>"; };
|
||||
8023C7413A426FBF0A52B684 /* RoundedCorner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCorner.swift; sourceTree = "<group>"; };
|
||||
804F9B0FABE093C7284CD09B /* TimelineItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemList.swift; sourceTree = "<group>"; };
|
||||
8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@ -626,16 +621,15 @@
|
||||
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
|
||||
8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = "<group>"; };
|
||||
8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomMessage.swift; sourceTree = "<group>"; };
|
||||
8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailProviderManager.swift; sourceTree = "<group>"; };
|
||||
8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = "<group>"; };
|
||||
8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = "<group>"; };
|
||||
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
|
||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||
9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = "<group>"; };
|
||||
90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; };
|
||||
92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = "<group>"; };
|
||||
92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = "<group>"; };
|
||||
9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = "<group>"; };
|
||||
938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
@ -655,6 +649,7 @@
|
||||
997783054A2E95F9E624217E /* kaa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kaa; path = kaa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
99DE232F24EAD72A3DF7EF1A /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kab; path = kab.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
9ABAECB0CA5FF8F8E6F10DD7 /* RoomTimelineProviderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderItem.swift; sourceTree = "<group>"; };
|
||||
9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurementParser.swift; sourceTree = "<group>"; };
|
||||
9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = "<group>"; };
|
||||
9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -669,6 +664,7 @@
|
||||
A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
A2B6433F516F1E6DFA0E2D89 /* vls */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vls; path = vls.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = "<group>"; };
|
||||
A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
|
||||
A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; };
|
||||
A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = "<group>"; };
|
||||
@ -680,6 +676,7 @@
|
||||
A8D1CC633517D695FEC54208 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
|
||||
A8F48EB9B52E70285A4BCB07 /* ur */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ur; path = ur.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
A9873374E72AA53260AE90A2 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
A9FAFE1C2149E6AC8156ED2B /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
|
||||
AA19C32BD97F45847724E09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Untranslated.strings; sourceTree = "<group>"; };
|
||||
AA8BA82CF99D843FEF680E91 /* AnalyticsPromptModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptModels.swift; sourceTree = "<group>"; };
|
||||
AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -710,7 +707,6 @@
|
||||
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = "<group>"; };
|
||||
B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenterSpy.swift; sourceTree = "<group>"; };
|
||||
B6BDAC8895AB2B77B47703AE /* MockRoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummary.swift; sourceTree = "<group>"; };
|
||||
B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
|
||||
B7E035C6AC137C9392D98814 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
B80D1901BA0B095E27793EDE /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@ -728,7 +724,6 @@
|
||||
C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = "<group>"; };
|
||||
C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachine.swift; sourceTree = "<group>"; };
|
||||
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
|
||||
C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProtocol.swift; sourceTree = "<group>"; };
|
||||
C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = "<group>"; };
|
||||
C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = "<group>"; };
|
||||
C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
|
||||
@ -744,12 +739,12 @@
|
||||
CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CBA95E52C4C6EE8769A63E57 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomMessage.swift; sourceTree = "<group>"; };
|
||||
CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JoinedRoomSize+MemberCount.swift"; sourceTree = "<group>"; };
|
||||
CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerProtocol.swift; sourceTree = "<group>"; };
|
||||
CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsRootView.swift; sourceTree = "<group>"; };
|
||||
CCF86010A0A719A9A50EEC59 /* SessionVerificationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationCoordinator.swift; sourceTree = "<group>"; };
|
||||
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||
CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CED34C87277BA3CCC6B6EC7A /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CF3EDF23226895776553F04A /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
||||
@ -796,10 +791,12 @@
|
||||
E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = "<group>"; };
|
||||
E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
|
||||
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
|
||||
EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModel.swift; sourceTree = "<group>"; };
|
||||
EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
EEAF1C75771D9DC75877F4B4 /* MessageTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTimelineItem.swift; sourceTree = "<group>"; };
|
||||
EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactoryProtocol.swift; sourceTree = "<group>"; };
|
||||
EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = "<group>"; };
|
||||
EF188681D6B6068CFAEAFC3F /* MXLogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogger.m; sourceTree = "<group>"; };
|
||||
@ -822,7 +819,6 @@
|
||||
FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; };
|
||||
FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = "<group>"; };
|
||||
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = "<group>"; };
|
||||
FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomMessage.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -1009,6 +1005,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */,
|
||||
8023C7413A426FBF0A52B684 /* RoundedCorner.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -1024,16 +1021,6 @@
|
||||
path = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33996F58948B54839D653EC1 /* Members */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */,
|
||||
6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */,
|
||||
09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */,
|
||||
);
|
||||
path = Members;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3510020809E49EFA146296AD /* ServerSelection */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1090,7 +1077,6 @@
|
||||
96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */,
|
||||
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */,
|
||||
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */,
|
||||
33996F58948B54839D653EC1 /* Members */,
|
||||
4658A940E89BC42EE3346A97 /* Messages */,
|
||||
70DABA39C844CA931B829395 /* RoomSummary */,
|
||||
);
|
||||
@ -1101,8 +1087,10 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B6E89E530A8E92EC44301CA1 /* Bundle.swift */,
|
||||
A9FAFE1C2149E6AC8156ED2B /* Collection.swift */,
|
||||
E26747B3154A5DBC3A7E24A5 /* Image.swift */,
|
||||
40B21E611DADDEF00307E7AC /* String.swift */,
|
||||
A40C19719687984FD9478FBE /* Task.swift */,
|
||||
287FC98AF2664EAD79C0D902 /* UIDevice.swift */,
|
||||
227AC5D71A4CE43512062243 /* URL.swift */,
|
||||
);
|
||||
@ -1120,11 +1108,7 @@
|
||||
4658A940E89BC42EE3346A97 /* Messages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */,
|
||||
8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */,
|
||||
CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */,
|
||||
607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */,
|
||||
4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */,
|
||||
);
|
||||
path = Messages;
|
||||
sourceTree = "<group>";
|
||||
@ -1186,6 +1170,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */,
|
||||
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@ -1228,6 +1213,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */,
|
||||
EEAF1C75771D9DC75877F4B4 /* MessageTimelineItem.swift */,
|
||||
);
|
||||
path = TimeLineItemContent;
|
||||
sourceTree = "<group>";
|
||||
@ -1288,12 +1274,10 @@
|
||||
70DABA39C844CA931B829395 /* RoomSummary */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4411C0DA0087A1CB143E96FA /* EventBrief.swift */,
|
||||
36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */,
|
||||
92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */,
|
||||
B6BDAC8895AB2B77B47703AE /* MockRoomSummary.swift */,
|
||||
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */,
|
||||
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */,
|
||||
C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */,
|
||||
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */,
|
||||
10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */,
|
||||
);
|
||||
path = RoomSummary;
|
||||
sourceTree = "<group>";
|
||||
@ -1868,9 +1852,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */,
|
||||
8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */,
|
||||
24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */,
|
||||
CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */,
|
||||
66F2402D738694F98729A441 /* RoomTimelineProvider.swift */,
|
||||
9ABAECB0CA5FF8F8E6F10DD7 /* RoomTimelineProviderItem.swift */,
|
||||
095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */,
|
||||
5A7A7D6D373D411C8C48B881 /* TimeLineItemContent */,
|
||||
95BE1C7CB2C80344FF0BE724 /* TimelineItems */,
|
||||
@ -2351,6 +2337,7 @@
|
||||
6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */,
|
||||
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */,
|
||||
24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */,
|
||||
663E198678778F7426A9B27D /* Collection.swift in Sources */,
|
||||
DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */,
|
||||
C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */,
|
||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */,
|
||||
@ -2359,13 +2346,9 @@
|
||||
9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */,
|
||||
D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */,
|
||||
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */,
|
||||
224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */,
|
||||
6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */,
|
||||
68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */,
|
||||
02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */,
|
||||
3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */,
|
||||
418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */,
|
||||
F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */,
|
||||
313382FC5D38064EAAA35CB2 /* FileManager.swift in Sources */,
|
||||
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */,
|
||||
85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */,
|
||||
@ -2373,11 +2356,11 @@
|
||||
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
||||
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */,
|
||||
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */,
|
||||
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */,
|
||||
DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */,
|
||||
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */,
|
||||
03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */,
|
||||
6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */,
|
||||
2E59008365E01F0AFB3A6B24 /* ImageRoomMessage.swift in Sources */,
|
||||
DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */,
|
||||
D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */,
|
||||
A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */,
|
||||
@ -2401,18 +2384,17 @@
|
||||
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */,
|
||||
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */,
|
||||
62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */,
|
||||
03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */,
|
||||
1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */,
|
||||
A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */,
|
||||
24906A1E82D0046655958536 /* MessageComposer.swift in Sources */,
|
||||
072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */,
|
||||
C35CF4DAB1467FE1BBDC204B /* MessageTimelineItem.swift in Sources */,
|
||||
152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */,
|
||||
28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */,
|
||||
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */,
|
||||
67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */,
|
||||
51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */,
|
||||
29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */,
|
||||
2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */,
|
||||
E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */,
|
||||
447E8580A0A2569E32529E17 /* MockRoomTimelineProvider.swift in Sources */,
|
||||
9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */,
|
||||
D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */,
|
||||
C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */,
|
||||
@ -2422,7 +2404,6 @@
|
||||
12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */,
|
||||
344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */,
|
||||
F56261126E368C831B3DE976 /* NavigationRouterType.swift in Sources */,
|
||||
33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */,
|
||||
8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */,
|
||||
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */,
|
||||
563A05B43207D00A6B698211 /* OIDCService.swift in Sources */,
|
||||
@ -2445,7 +2426,8 @@
|
||||
7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */,
|
||||
617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */,
|
||||
59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */,
|
||||
BE3237142FA6E1A13C0E7D11 /* RoomSummaryProtocol.swift in Sources */,
|
||||
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */,
|
||||
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */,
|
||||
78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */,
|
||||
9B8DE1D424E37581C7D99CCC /* RoomTimelineControllerProtocol.swift in Sources */,
|
||||
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */,
|
||||
@ -2453,12 +2435,14 @@
|
||||
C8E82786DE1B6A400DA9BA25 /* RoomTimelineItemProperties.swift in Sources */,
|
||||
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */,
|
||||
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */,
|
||||
A0A26E4F713596856470EF1A /* RoomTimelineProviderItem.swift in Sources */,
|
||||
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */,
|
||||
5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */,
|
||||
297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */,
|
||||
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */,
|
||||
7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */,
|
||||
2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */,
|
||||
F764BE976EAB76D63E7C1678 /* RoundedCorner.swift in Sources */,
|
||||
462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */,
|
||||
CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */,
|
||||
1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */,
|
||||
@ -2498,13 +2482,13 @@
|
||||
2F94054F50E312AF30BE07F3 /* String.swift in Sources */,
|
||||
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */,
|
||||
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */,
|
||||
E290C78E7F09F47FD2662986 /* Task.swift in Sources */,
|
||||
43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */,
|
||||
63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */,
|
||||
1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */,
|
||||
75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */,
|
||||
5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */,
|
||||
D85D4FA590305180B4A41795 /* Tests.swift in Sources */,
|
||||
D013E70C8E28E43497820444 /* TextRoomMessage.swift in Sources */,
|
||||
7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */,
|
||||
5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */,
|
||||
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */,
|
||||
@ -3165,7 +3149,7 @@
|
||||
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = "1.0.13-alpha";
|
||||
version = "0.0.4-demo";
|
||||
};
|
||||
};
|
||||
96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
|
||||
|
@ -77,8 +77,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "1358c9c2a85cfb5fc1bfadce13565e02618725d4",
|
||||
"version" : "1.0.13-alpha"
|
||||
"revision" : "b10003bc8537f95da0197627fda977cb9dec98f9",
|
||||
"version" : "0.0.4-demo"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -2,10 +2,12 @@
|
||||
"untranslated" = "Untranslated";
|
||||
|
||||
"action_confirm" = "Confirm";
|
||||
"action_match" = "Match";
|
||||
|
||||
"screenshot_detected_title" = "You took a screenshot";
|
||||
"screenshot_detected_message" = "Would you like to submit a bug report?";
|
||||
|
||||
"settings_appearance" = "Appearance";
|
||||
"settings_timeline_style" = "Timeline Style";
|
||||
"room_timeline_style_plain_long_description" = "Plain Timeline";
|
||||
"room_timeline_style_bubbled_long_description" = "Bubbled Timeline";
|
||||
@ -14,6 +16,13 @@
|
||||
|
||||
"room_timeline_replying_to" = "Replying to %@";
|
||||
|
||||
"session_verification_banner_title" = "Help keep your messages secure";
|
||||
"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you.";
|
||||
"session_verification_screen_emojis_title" = "Lets check if these";
|
||||
"session_verification_screen_emojis_message" = "Open Element on one of your other sessions to compare.";
|
||||
|
||||
"home_screen_all_chats" = "All Chats";
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
"authentication_login_title" = "Welcome back!";
|
||||
|
@ -52,8 +52,6 @@ class AppCoordinator: Coordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private let memberDetailProviderManager: MemberDetailProviderManager
|
||||
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let screenshotDetector: ScreenshotDetector
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
@ -71,15 +69,16 @@ class AppCoordinator: Coordinator {
|
||||
bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL)
|
||||
|
||||
splashViewController = SplashViewController()
|
||||
|
||||
mainNavigationController = ElementNavigationController(rootViewController: splashViewController)
|
||||
mainNavigationController.navigationBar.prefersLargeTitles = true
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = mainNavigationController
|
||||
window.tintColor = .element.accent
|
||||
|
||||
navigationRouter = NavigationRouter(navigationController: mainNavigationController)
|
||||
|
||||
memberDetailProviderManager = MemberDetailProviderManager()
|
||||
|
||||
ServiceLocator.serviceLocator = ServiceLocator(userIndicatorPresenter: UserIndicatorTypePresenter(presentingViewController: mainNavigationController))
|
||||
|
||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
|
||||
@ -115,7 +114,7 @@ class AppCoordinator: Coordinator {
|
||||
// This exposes the full Rust side tracing subscriber filter for more flexibility.
|
||||
// We can filter by level, crate and even file. See more details here:
|
||||
// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples
|
||||
setupTracing(configuration: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
setupTracing(configuration: "warn,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
|
||||
loggerConfiguration.logLevel = .debug
|
||||
#else
|
||||
@ -131,7 +130,7 @@ class AppCoordinator: Coordinator {
|
||||
MXLog.configure(loggerConfiguration)
|
||||
}
|
||||
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
private func setupStateMachine() {
|
||||
stateMachine.addTransitionHandler { [weak self] context in
|
||||
guard let self = self else { return }
|
||||
@ -171,12 +170,7 @@ class AppCoordinator: Coordinator {
|
||||
case (.remoteSigningOut(let isSoft), .completedSigningOut, .signedOut):
|
||||
self.presentSplashScreen(isSoftLogout: isSoft)
|
||||
self.hideLoadingIndicator()
|
||||
|
||||
case (.homeScreen, .showSettingsScreen, .settingsScreen):
|
||||
self.presentSettingsScreen()
|
||||
case (.settingsScreen, .dismissedSettingsScreen, .homeScreen):
|
||||
self.tearDownDismissedSettingsScreen()
|
||||
|
||||
|
||||
case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
self.presentSessionVerification()
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen):
|
||||
@ -191,8 +185,6 @@ class AppCoordinator: Coordinator {
|
||||
fatalError("Failed transition with context: \(context)")
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable cyclomatic_complexity function_body_length
|
||||
|
||||
private func restoreUserSession() {
|
||||
Task {
|
||||
@ -265,21 +257,23 @@ class AppCoordinator: Coordinator {
|
||||
}
|
||||
|
||||
private func tearDownUserSession(isSoftLogout: Bool = false) {
|
||||
Task {
|
||||
deobserveUserSessionChanges()
|
||||
|
||||
if !isSoftLogout {
|
||||
userSession.clientProxy.stopSync()
|
||||
|
||||
deobserveUserSessionChanges()
|
||||
|
||||
if !isSoftLogout {
|
||||
Task {
|
||||
// first log out from the server
|
||||
_ = await userSession.clientProxy.logout()
|
||||
|
||||
|
||||
// regardless of the result, clear user data
|
||||
userSessionStore.logout(userSession: userSession)
|
||||
userSession = nil
|
||||
}
|
||||
|
||||
// complete logging out
|
||||
stateMachine.processEvent(.completedSigningOut)
|
||||
}
|
||||
|
||||
// complete logging out
|
||||
stateMachine.processEvent(.completedSigningOut)
|
||||
}
|
||||
|
||||
private func presentSplashScreen(isSoftLogout: Bool = false) {
|
||||
@ -297,9 +291,10 @@ class AppCoordinator: Coordinator {
|
||||
}
|
||||
|
||||
private func presentHomeScreen() {
|
||||
userSession.clientProxy.startSync()
|
||||
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder(),
|
||||
memberDetailProviderManager: memberDetailProviderManager)
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
@ -309,7 +304,7 @@ class AppCoordinator: Coordinator {
|
||||
case .presentRoom(let roomIdentifier):
|
||||
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
|
||||
case .presentSettings:
|
||||
self.stateMachine.processEvent(.showSettingsScreen)
|
||||
self.presentSettingsScreen()
|
||||
case .presentBugReport:
|
||||
self.presentBugReportScreen()
|
||||
case .verifySession:
|
||||
@ -353,31 +348,29 @@ class AppCoordinator: Coordinator {
|
||||
// MARK: Rooms
|
||||
|
||||
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
|
||||
guard let roomProxy = userSession.clientProxy.rooms.first(where: { $0.id == roomIdentifier }) else {
|
||||
guard let roomProxy = userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
|
||||
MXLog.error("Invalid room identifier: \(roomIdentifier)")
|
||||
return
|
||||
}
|
||||
let userId = userSession.clientProxy.userIdentifier
|
||||
|
||||
let memberDetailProvider = memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy)
|
||||
|
||||
let timelineItemFactory = RoomTimelineItemFactory(mediaProvider: userSession.mediaProvider,
|
||||
memberDetailProvider: memberDetailProvider,
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
|
||||
|
||||
let timelineController = RoomTimelineController(userId: userId,
|
||||
roomId: roomIdentifier,
|
||||
timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
|
||||
timelineProvider: roomProxy.timelineProvider,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
memberDetailProvider: memberDetailProvider)
|
||||
|
||||
roomProxy: roomProxy)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
|
||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||
roomAvatar: userSession.mediaProvider.imageFromURLString(roomProxy.avatarURL),
|
||||
roomEncryptionBadge: roomProxy.encryptionBadgeImage)
|
||||
roomAvatar: userSession.mediaProvider.imageFromURLString(roomProxy.avatarURL, size: MediaProviderDefaultAvatarSize))
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
@ -396,32 +389,39 @@ class AppCoordinator: Coordinator {
|
||||
// MARK: Settings
|
||||
|
||||
private func presentSettingsScreen() {
|
||||
let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
let navController = ElementNavigationController()
|
||||
let newNavigationRouter = NavigationRouter(navigationController: navController)
|
||||
|
||||
let parameters = SettingsCoordinatorParameters(navigationRouter: newNavigationRouter,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let coordinator = SettingsCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.dismissSettingsScreen()
|
||||
case .logout:
|
||||
self.dismissSettingsScreen()
|
||||
self.stateMachine.processEvent(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.stateMachine.processEvent(.dismissedSettingsScreen)
|
||||
}
|
||||
navController.viewControllers = [coordinator.toPresentable()]
|
||||
navigationRouter.present(navController, animated: true)
|
||||
}
|
||||
|
||||
private func tearDownDismissedSettingsScreen() {
|
||||
guard let coordinator = childCoordinators.last as? SettingsCoordinator else {
|
||||
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
|
||||
|
||||
@objc
|
||||
private func dismissSettingsScreen() {
|
||||
MXLog.debug("dismissSettingsScreen")
|
||||
|
||||
guard let coordinator = childCoordinators.first(where: { $0 is SettingsCoordinator }) else {
|
||||
return
|
||||
}
|
||||
|
||||
navigationRouter.dismissModule()
|
||||
remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
|
@ -35,10 +35,7 @@ class AppCoordinatorStateMachine {
|
||||
|
||||
/// Showing the session verification flows
|
||||
case sessionVerificationScreen
|
||||
|
||||
/// Showing the settings screen
|
||||
case settingsScreen
|
||||
|
||||
|
||||
/// Processing a sign out request
|
||||
case signingOut
|
||||
|
||||
@ -77,11 +74,6 @@ class AppCoordinatorStateMachine {
|
||||
case showSessionVerificationScreen
|
||||
/// Session verification has finished
|
||||
case dismissedSessionVerificationScreen
|
||||
|
||||
/// Request settings screen presentation
|
||||
case showSettingsScreen
|
||||
/// The settings screen has been dismissed
|
||||
case dismissedSettingsScreen
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
@ -97,9 +89,6 @@ class AppCoordinatorStateMachine {
|
||||
|
||||
machine.addRoutes(event: .signOut, transitions: [.any => .signingOut])
|
||||
machine.addRoutes(event: .completedSigningOut, transitions: [.signingOut => .signedOut])
|
||||
|
||||
machine.addRoutes(event: .showSettingsScreen, transitions: [.homeScreen => .settingsScreen])
|
||||
machine.addRoutes(event: .dismissedSettingsScreen, transitions: [.settingsScreen => .homeScreen])
|
||||
|
||||
machine.addRoutes(event: .showSessionVerificationScreen, transitions: [.homeScreen => .sessionVerificationScreen])
|
||||
machine.addRoutes(event: .dismissedSessionVerificationScreen, transitions: [.sessionVerificationScreen => .homeScreen])
|
||||
|
@ -20,6 +20,7 @@ final class BuildSettings {
|
||||
// MARK: - Servers
|
||||
|
||||
static let defaultHomeserverAddress = "matrix.org"
|
||||
static let slidingSyncProxyBaseURL = URL(staticString: "https://slidingsync.lab.element.dev")
|
||||
|
||||
// MARK: - Bug report
|
||||
|
||||
|
@ -12,6 +12,8 @@ import Foundation
|
||||
extension ElementL10n {
|
||||
/// Confirm
|
||||
public static let actionConfirm = ElementL10n.tr("Untranslated", "action_confirm")
|
||||
/// Match
|
||||
public static let actionMatch = ElementL10n.tr("Untranslated", "action_match")
|
||||
/// Forgot password
|
||||
public static let authenticationLoginForgotPassword = ElementL10n.tr("Untranslated", "authentication_login_forgot_password")
|
||||
/// Welcome back!
|
||||
@ -20,6 +22,8 @@ extension ElementL10n {
|
||||
public static let authenticationServerInfoMatrixDescription = ElementL10n.tr("Untranslated", "authentication_server_info_matrix_description")
|
||||
/// Choose your server to store your data
|
||||
public static let authenticationServerInfoTitle = ElementL10n.tr("Untranslated", "authentication_server_info_title")
|
||||
/// All Chats
|
||||
public static let homeScreenAllChats = ElementL10n.tr("Untranslated", "home_screen_all_chats")
|
||||
/// Mobile
|
||||
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
||||
/// Tablet
|
||||
@ -48,6 +52,16 @@ extension ElementL10n {
|
||||
public static let serverSelectionServerUrl = ElementL10n.tr("Untranslated", "server_selection_server_url")
|
||||
/// Choose your server
|
||||
public static let serverSelectionTitle = ElementL10n.tr("Untranslated", "server_selection_title")
|
||||
/// Looks like you’re using a new device. Verify its you.
|
||||
public static let sessionVerificationBannerMessage = ElementL10n.tr("Untranslated", "session_verification_banner_message")
|
||||
/// Help keep your messages secure
|
||||
public static let sessionVerificationBannerTitle = ElementL10n.tr("Untranslated", "session_verification_banner_title")
|
||||
/// Open Element on one of your other sessions to compare.
|
||||
public static let sessionVerificationScreenEmojisMessage = ElementL10n.tr("Untranslated", "session_verification_screen_emojis_message")
|
||||
/// Lets check if these
|
||||
public static let sessionVerificationScreenEmojisTitle = ElementL10n.tr("Untranslated", "session_verification_screen_emojis_title")
|
||||
/// Appearance
|
||||
public static let settingsAppearance = ElementL10n.tr("Untranslated", "settings_appearance")
|
||||
/// Timeline Style
|
||||
public static let settingsTimelineStyle = ElementL10n.tr("Untranslated", "settings_timeline_style")
|
||||
/// Untranslated
|
||||
|
@ -16,7 +16,9 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol EventBriefFactoryProtocol {
|
||||
func buildEventBriefFor(message: RoomMessageProtocol?) async -> EventBrief?
|
||||
extension Collection {
|
||||
/// Returns the element at the specified index if it is within bounds, otherwise nil.
|
||||
subscript(safe index: Index) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
@ -39,4 +39,16 @@ extension String {
|
||||
let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive)
|
||||
return detector?.numberOfMatches(in: self, range: range) ?? 0 == 1
|
||||
}
|
||||
|
||||
/// Calculates a numeric hash same as Element Web
|
||||
/// See original function here https://github.com/matrix-org/matrix-react-sdk/blob/321dd49db4fbe360fc2ff109ac117305c955b061/src/utils/FormattingUtils.js#L47
|
||||
var hashCode: Int32 {
|
||||
var hash: Int32 = 0
|
||||
|
||||
for character in self {
|
||||
let shiftedHash = hash << 5
|
||||
hash = shiftedHash.subtractingReportingOverflow(hash).partialValue + Int32(character.unicodeScalars[character.unicodeScalars.startIndex].value)
|
||||
}
|
||||
return abs(hash)
|
||||
}
|
||||
}
|
||||
|
50
ElementX/Sources/Other/Extensions/Task.swift
Normal file
50
ElementX/Sources/Other/Extensions/Task.swift
Normal file
@ -0,0 +1,50 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension Task where Failure == Never {
|
||||
static func dispatched(on queue: DispatchQueue,
|
||||
priority: TaskPriority? = nil,
|
||||
operation: @escaping @Sendable () -> Success) -> Task<Success, Failure> {
|
||||
Task.detached(priority: priority) {
|
||||
await withCheckedContinuation { continuation in
|
||||
queue.async {
|
||||
continuation.resume(returning: operation())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Task where Failure == Error {
|
||||
static func dispatched(on queue: DispatchQueue,
|
||||
priority: TaskPriority? = nil,
|
||||
operation: @escaping @Sendable () throws -> Success) -> Task<Success, Failure> {
|
||||
Task.detached(priority: priority) {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
queue.async {
|
||||
do {
|
||||
let result = try operation()
|
||||
continuation.resume(returning: result)
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
ElementX/Sources/Other/SwiftUI/Views/RoundedCorner.swift
Normal file
48
ElementX/Sources/Other/SwiftUI/Views/RoundedCorner.swift
Normal file
@ -0,0 +1,48 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct RoundedCorner: Shape {
|
||||
let radius: CGFloat
|
||||
let corners: UIRectCorner
|
||||
|
||||
init(radius: CGFloat, corners: UIRectCorner) {
|
||||
self.radius = radius
|
||||
self.corners = corners
|
||||
}
|
||||
|
||||
init(radius: CGFloat, inGroupState: TimelineItemInGroupState) {
|
||||
self.init(radius: radius, corners: inGroupState.roundedCorners)
|
||||
}
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(roundedRect: rect,
|
||||
byRoundingCorners: corners,
|
||||
cornerRadii: CGSize(width: radius, height: radius))
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
clipShape(RoundedCorner(radius: radius, corners: corners))
|
||||
}
|
||||
|
||||
func cornerRadius(_ radius: CGFloat, inGroupState: TimelineItemInGroupState) -> some View {
|
||||
clipShape(RoundedCorner(radius: radius, inGroupState: inGroupState))
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
|
@ -15,7 +15,6 @@
|
||||
//
|
||||
|
||||
import AppAuth
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct LoginCoordinatorParameters {
|
||||
|
@ -15,7 +15,6 @@
|
||||
//
|
||||
|
||||
import AppAuth
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct SoftLogoutCoordinatorParameters {
|
||||
|
@ -20,7 +20,6 @@ import SwiftUI
|
||||
struct HomeScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
let memberDetailProviderManager: MemberDetailProviderManager
|
||||
}
|
||||
|
||||
enum HomeScreenCoordinatorAction {
|
||||
@ -40,8 +39,6 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
private let hostingController: UIViewController
|
||||
private var viewModel: HomeScreenViewModelProtocol
|
||||
|
||||
private var roomSummaries: [RoomSummary] = []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
@ -55,7 +52,8 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
init(parameters: HomeScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = HomeScreenViewModel(attributedStringBuilder: parameters.attributedStringBuilder)
|
||||
viewModel = HomeScreenViewModel(userSession: parameters.userSession,
|
||||
attributedStringBuilder: parameters.attributedStringBuilder)
|
||||
|
||||
let view = HomeScreen(context: viewModel.context)
|
||||
hostingController = UIHostingController(rootView: view)
|
||||
@ -72,42 +70,10 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
self.callback?(.verifySession)
|
||||
}
|
||||
}
|
||||
|
||||
parameters.userSession.clientProxy
|
||||
.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
if case .updatedRoomsList = callback {
|
||||
self?.updateRoomsList()
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
parameters.userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
switch callback {
|
||||
case .sessionVerificationNeeded:
|
||||
self?.viewModel.showSessionVerificationBanner()
|
||||
case .didVerifySession:
|
||||
self?.viewModel.hideSessionVerificationBanner()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
updateRoomsList()
|
||||
|
||||
Task {
|
||||
if case let .success(userAvatarURLString) = await parameters.userSession.clientProxy.loadUserAvatarURLString() {
|
||||
if case let .success(avatar) = await parameters.userSession.mediaProvider.loadImageFromURLString(userAvatarURLString) {
|
||||
self.viewModel.updateWithUserAvatar(avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
|
||||
func start() { }
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
@ -116,26 +82,6 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
func updateRoomsList() {
|
||||
roomSummaries = parameters.userSession.clientProxy.rooms.compactMap { roomProxy in
|
||||
guard roomProxy.isJoined, !roomProxy.isSpace, !roomProxy.isTombstoned else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let summary = self.roomSummaries.first(where: { $0.id == roomProxy.id }) {
|
||||
return summary
|
||||
}
|
||||
|
||||
let memberDetailProvider = parameters.memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy)
|
||||
|
||||
return RoomSummary(roomProxy: roomProxy,
|
||||
mediaProvider: parameters.userSession.mediaProvider,
|
||||
eventBriefFactory: EventBriefFactory(memberDetailProvider: memberDetailProvider))
|
||||
}
|
||||
|
||||
viewModel.updateWithRoomSummaries(roomSummaries)
|
||||
}
|
||||
|
||||
private func processUserMenuAction(_ action: HomeScreenViewUserMenuAction) {
|
||||
switch action {
|
||||
case .settings:
|
||||
|
@ -35,6 +35,7 @@ enum HomeScreenViewAction {
|
||||
case selectRoom(roomIdentifier: String)
|
||||
case userMenu(action: HomeScreenViewUserMenuAction)
|
||||
case verifySession
|
||||
case skipSessionVerification
|
||||
}
|
||||
|
||||
struct HomeScreenViewState: BindableState {
|
||||
@ -44,24 +45,16 @@ struct HomeScreenViewState: BindableState {
|
||||
|
||||
var rooms: [HomeScreenRoom] = []
|
||||
|
||||
var isLoadingRooms = false
|
||||
|
||||
var visibleDMs: [HomeScreenRoom] {
|
||||
searchFilteredRooms.filter(\.isDirect)
|
||||
var isLoadingRooms: Bool {
|
||||
rooms.isEmpty
|
||||
}
|
||||
|
||||
|
||||
var visibleRooms: [HomeScreenRoom] {
|
||||
searchFilteredRooms.filter { !$0.isDirect }
|
||||
}
|
||||
|
||||
private var searchFilteredRooms: LazyFilterSequence<LazySequence<[HomeScreenRoom]>.Elements> {
|
||||
guard !bindings.searchQuery.isEmpty else {
|
||||
// This extra filter is fine for now as there are always downstream filters
|
||||
// but if that changes, this approach should be reconsidered.
|
||||
return rooms.lazy.filter { _ in true }
|
||||
if bindings.searchQuery.isEmpty {
|
||||
return rooms
|
||||
}
|
||||
|
||||
return rooms.lazy.filter { $0.displayName?.localizedStandardContains(bindings.searchQuery) ?? false }
|
||||
return rooms.lazy.filter { $0.name.localizedStandardContains(bindings.searchQuery) }
|
||||
}
|
||||
|
||||
var bindings = HomeScreenViewStateBindings()
|
||||
@ -74,17 +67,15 @@ struct HomeScreenViewStateBindings {
|
||||
struct HomeScreenRoom: Identifiable, Equatable {
|
||||
let id: String
|
||||
|
||||
var displayName: String?
|
||||
let name: String
|
||||
|
||||
var topic: String?
|
||||
var lastMessage: String?
|
||||
let hasUnreads: Bool
|
||||
|
||||
let timestamp: String?
|
||||
|
||||
var lastMessage: AttributedString?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
let isDirect: Bool
|
||||
let isEncrypted: Bool
|
||||
let isSpace: Bool
|
||||
let isTombstoned: Bool
|
||||
}
|
||||
|
||||
extension MutableCollection where Element == HomeScreenRoom {
|
||||
|
@ -20,29 +20,54 @@ import SwiftUI
|
||||
typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState, HomeScreenViewAction>
|
||||
|
||||
class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let roomSummaryProvider: RoomSummaryProviderProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
|
||||
private var roomUpdateListeners = Set<AnyCancellable>()
|
||||
private var roomsUpdateTask: Task<Void, Never>? {
|
||||
willSet {
|
||||
roomsUpdateTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private var roomSummaries: [RoomSummaryProtocol]? {
|
||||
didSet {
|
||||
state.isLoadingRooms = (roomSummaries?.count ?? 0 == 0)
|
||||
}
|
||||
}
|
||||
|
||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
self.userSession = userSession
|
||||
roomSummaryProvider = userSession.clientProxy.roomSummaryProvider
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
|
||||
super.init(initialViewState: HomeScreenViewState(isLoadingRooms: true))
|
||||
super.init(initialViewState: HomeScreenViewState())
|
||||
|
||||
userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
switch callback {
|
||||
case .sessionVerificationNeeded:
|
||||
self?.state.showSessionVerificationBanner = true
|
||||
case .didVerifySession:
|
||||
self?.state.showSessionVerificationBanner = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
roomSummaryProvider.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
switch callback {
|
||||
case .updatedRoomSummaries:
|
||||
Task {
|
||||
await self?.updateRooms()
|
||||
}
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() {
|
||||
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, size: MediaProviderDefaultAvatarSize) {
|
||||
state.userAvatar = avatar
|
||||
}
|
||||
}
|
||||
|
||||
await updateRooms()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@ -50,124 +75,63 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
override func process(viewAction: HomeScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .loadRoomData(let roomIdentifier):
|
||||
loadRoomDataForIdentifier(roomIdentifier)
|
||||
loadDataForRoomIdentifier(roomIdentifier)
|
||||
case .selectRoom(let roomIdentifier):
|
||||
callback?(.selectRoom(roomIdentifier: roomIdentifier))
|
||||
case .userMenu(let action):
|
||||
callback?(.userMenu(action: action))
|
||||
case .verifySession:
|
||||
callback?(.verifySession)
|
||||
case .skipSessionVerification:
|
||||
state.showSessionVerificationBanner = false
|
||||
}
|
||||
}
|
||||
|
||||
func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) {
|
||||
roomsUpdateTask = Task {
|
||||
await updateWithRoomSummaries(roomSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) async {
|
||||
var rooms = [HomeScreenRoom]()
|
||||
for summary in roomSummaries {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
rooms.append(await buildOrUpdateRoomForSummary(summary))
|
||||
}
|
||||
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
state.rooms = rooms
|
||||
self.roomSummaries = roomSummaries
|
||||
|
||||
roomUpdateListeners.removeAll()
|
||||
roomSummaries.forEach { roomSummary in
|
||||
roomSummary.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
if let index = self.state.rooms.firstIndex(where: { $0.id == roomSummary.id }) {
|
||||
switch callback {
|
||||
case .updatedLastMessage:
|
||||
var room = self.state.rooms[index]
|
||||
room.lastMessage = await self.lastMessageFromEventBrief(roomSummary.lastMessage)
|
||||
self.state.rooms[index] = room
|
||||
default:
|
||||
self.state.rooms[index] = await self.buildOrUpdateRoomForSummary(roomSummary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &roomUpdateListeners)
|
||||
}
|
||||
}
|
||||
|
||||
func updateWithUserAvatar(_ avatar: UIImage) {
|
||||
state.userAvatar = avatar
|
||||
}
|
||||
|
||||
func showSessionVerificationBanner() {
|
||||
state.showSessionVerificationBanner = true
|
||||
}
|
||||
|
||||
func hideSessionVerificationBanner() {
|
||||
state.showSessionVerificationBanner = false
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadRoomDataForIdentifier(_ roomIdentifier: String) {
|
||||
guard let roomSummary = roomSummaries?.first(where: { $0.id == roomIdentifier }) else {
|
||||
MXLog.error("Invalid room identifier")
|
||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||
guard let summary = roomSummaryProvider.roomSummaries.first(where: { $0.id == identifier }),
|
||||
var room = state.rooms.first(where: { $0.id == identifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
Task { await roomSummary.loadDetails() }
|
||||
}
|
||||
|
||||
private func buildOrUpdateRoomForSummary(_ roomSummary: RoomSummaryProtocol) async -> HomeScreenRoom {
|
||||
guard var room = state.rooms.first(where: { $0.id == roomSummary.id }) else {
|
||||
return HomeScreenRoom(id: roomSummary.id,
|
||||
displayName: roomSummary.displayName,
|
||||
topic: roomSummary.topic,
|
||||
lastMessage: await lastMessageFromEventBrief(roomSummary.lastMessage),
|
||||
avatar: roomSummary.avatar,
|
||||
isDirect: roomSummary.isDirect,
|
||||
isEncrypted: roomSummary.isEncrypted,
|
||||
isSpace: roomSummary.isSpace,
|
||||
isTombstoned: roomSummary.isTombstoned)
|
||||
if room.avatar != nil {
|
||||
return
|
||||
}
|
||||
|
||||
room.avatar = roomSummary.avatar
|
||||
room.displayName = roomSummary.displayName
|
||||
room.topic = roomSummary.topic
|
||||
if let avatarURLString = summary.avatarURLString {
|
||||
Task {
|
||||
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, size: MediaProviderDefaultAvatarSize) {
|
||||
if let index = state.rooms.firstIndex(of: room) {
|
||||
room.avatar = image
|
||||
state.rooms[index] = room
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRooms() async {
|
||||
state.rooms = await Task.detached {
|
||||
var rooms = [HomeScreenRoom]()
|
||||
|
||||
for summary in self.roomSummaryProvider.roomSummaries {
|
||||
let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, size: MediaProviderDefaultAvatarSize)
|
||||
|
||||
return room
|
||||
}
|
||||
|
||||
private func lastMessageFromEventBrief(_ eventBrief: EventBrief?) async -> String? {
|
||||
guard let eventBrief = eventBrief else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let senderDisplayName = senderDisplayNameForBrief(eventBrief)
|
||||
|
||||
if let htmlBody = eventBrief.htmlBody,
|
||||
let lastMessageAttributedString = await attributedStringBuilder.fromHTML(htmlBody) {
|
||||
return "\(senderDisplayName): \(String(lastMessageAttributedString.characters))"
|
||||
} else {
|
||||
return "\(senderDisplayName): \(eventBrief.body)"
|
||||
}
|
||||
}
|
||||
|
||||
private func senderDisplayNameForBrief(_ brief: EventBrief) -> String {
|
||||
brief.senderDisplayName ?? brief.senderId
|
||||
var timestamp: String?
|
||||
if let lastMessageTimestamp = summary.lastMessageTimestamp {
|
||||
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
rooms.append(HomeScreenRoom(id: summary.id,
|
||||
name: summary.name,
|
||||
hasUnreads: summary.unreadNotificationCount > 0,
|
||||
timestamp: timestamp,
|
||||
lastMessage: summary.lastMessage,
|
||||
avatar: avatarImage))
|
||||
}
|
||||
|
||||
return rooms
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,4 @@ protocol HomeScreenViewModelProtocol {
|
||||
var callback: ((HomeScreenViewModelAction) -> Void)? { get set }
|
||||
|
||||
var context: HomeScreenViewModelType.Context { get }
|
||||
|
||||
func updateWithUserAvatar(_ avatar: UIImage)
|
||||
func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol])
|
||||
|
||||
func showSessionVerificationBanner()
|
||||
func hideSessionVerificationBanner()
|
||||
}
|
||||
|
@ -29,36 +29,20 @@ struct HomeScreen: View {
|
||||
ProgressView()
|
||||
}
|
||||
} else {
|
||||
if context.viewState.showSessionVerificationBanner {
|
||||
HStack {
|
||||
Text(ElementL10n.verificationVerifyDevice)
|
||||
Spacer()
|
||||
Button(ElementL10n.startVerification) {
|
||||
context.send(viewAction: .verifySession)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.element.quaternaryContent)
|
||||
.padding(.top, 1)
|
||||
}
|
||||
|
||||
List {
|
||||
Section(ElementL10n.rooms) {
|
||||
ForEach(context.viewState.visibleRooms) { room in
|
||||
RoomCell(room: room, context: context)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
ScrollView {
|
||||
if context.viewState.showSessionVerificationBanner {
|
||||
sessionVerificationBanner
|
||||
}
|
||||
|
||||
Section(ElementL10n.bottomActionPeople) {
|
||||
ForEach(context.viewState.visibleDMs) { room in
|
||||
RoomCell(room: room, context: context)
|
||||
.listRowBackground(Color.clear)
|
||||
LazyVStack {
|
||||
ForEach(context.viewState.visibleRooms) { room in
|
||||
HomeScreenRoomCell(room: room, context: context)
|
||||
}
|
||||
}
|
||||
.searchable(text: $context.searchQuery)
|
||||
.animation(.default, value: context.viewState.visibleRooms)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.searchable(text: $context.searchQuery)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@ -66,7 +50,7 @@ struct HomeScreen: View {
|
||||
.transition(.slide)
|
||||
.animation(.elementDefault, value: context.viewState.showSessionVerificationBanner)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(ElementL10n.homeScreenAllChats)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
userMenuButton
|
||||
@ -119,6 +103,37 @@ struct HomeScreen: View {
|
||||
return .empty
|
||||
}
|
||||
}
|
||||
|
||||
private var sessionVerificationBanner: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(ElementL10n.sessionVerificationBannerTitle)
|
||||
.font(.element.subheadlineBold)
|
||||
.foregroundColor(.element.systemPrimaryLabel)
|
||||
Text(ElementL10n.sessionVerificationBannerMessage)
|
||||
.font(.element.footnote)
|
||||
.foregroundColor(.element.systemSecondaryLabel)
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Button(ElementL10n.actionSkip) {
|
||||
context.send(viewAction: .skipSessionVerification)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.elementCapsule)
|
||||
|
||||
Button(ElementL10n.continue) {
|
||||
context.send(viewAction: .verifySession)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.elementCapsuleProminent)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.element.systemSecondaryBackground)
|
||||
.cornerRadius(14)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
private func settings() {
|
||||
context.send(viewAction: .userMenu(action: .settings))
|
||||
@ -137,62 +152,6 @@ struct HomeScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomCell: View {
|
||||
@ScaledMetric private var avatarSize = 32.0
|
||||
|
||||
let room: HomeScreenRoom
|
||||
let context: HomeScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: room.id))
|
||||
} label: {
|
||||
HStack(spacing: 16.0) {
|
||||
if let avatar = room.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: room.displayName ?? room.id)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2.0) {
|
||||
Text(roomName(room))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if let roomTopic = room.topic, roomTopic.count > 0 {
|
||||
Text(roomTopic)
|
||||
.font(.footnote.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let lastMessage = room.lastMessage {
|
||||
Text(lastMessage)
|
||||
.font(.callout)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.elementDefault, value: room)
|
||||
.frame(minHeight: 60.0)
|
||||
.task {
|
||||
context.send(viewAction: .loadRoomData(roomIdentifier: room.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func roomName(_ room: HomeScreenRoom) -> String {
|
||||
room.displayName ?? room.id + (room.isEncrypted ? "🛡" : "")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct HomeScreen_Previews: PreviewProvider {
|
||||
@ -202,28 +161,13 @@ struct HomeScreen_Previews: PreviewProvider {
|
||||
body.preferredColorScheme(.dark)
|
||||
.tint(.element.accent)
|
||||
}
|
||||
|
||||
|
||||
static var body: some View {
|
||||
let viewModel = HomeScreenViewModel(attributedStringBuilder: AttributedStringBuilder())
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
let eventBrief = EventBrief(eventId: "id",
|
||||
senderId: "senderId",
|
||||
senderDisplayName: "Sender",
|
||||
body: "Some message",
|
||||
htmlBody: nil,
|
||||
date: .now)
|
||||
|
||||
let roomSummaries = [MockRoomSummary(displayName: "Alpha", topic: "Topic"),
|
||||
MockRoomSummary(displayName: "Beta"),
|
||||
MockRoomSummary(displayName: "Omega", lastMessage: eventBrief)]
|
||||
|
||||
viewModel.updateWithRoomSummaries(roomSummaries)
|
||||
|
||||
if let avatarImage = UIImage(systemName: "person.fill") {
|
||||
viewModel.updateWithUserAvatar(avatarImage)
|
||||
}
|
||||
|
||||
viewModel.showSessionVerificationBanner()
|
||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
|
||||
return NavigationView {
|
||||
HomeScreen(context: viewModel.context)
|
||||
|
@ -0,0 +1,123 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct HomeScreenRoomCell: View {
|
||||
@ScaledMetric private var avatarSize = 44.0
|
||||
|
||||
let room: HomeScreenRoom
|
||||
let context: HomeScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: room.id))
|
||||
} label: {
|
||||
HStack(spacing: 16.0) {
|
||||
if let avatar = room.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: room.name, contentId: room.id)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
}
|
||||
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 2.0) {
|
||||
Text(room.name)
|
||||
.font(.element.callout.bold())
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.lineLimit(1)
|
||||
|
||||
if let lastMessage = room.lastMessage {
|
||||
Text(lastMessage)
|
||||
.font(lastMessageFont)
|
||||
.foregroundColor(lastMessageForegroundColor)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.top, 2)
|
||||
.animation(nil, value: UUID()) // Text animations look ugly
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 3.0) {
|
||||
if let timestamp = room.timestamp {
|
||||
Text(timestamp)
|
||||
.font(.element.caption1)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
}
|
||||
|
||||
if room.hasUnreads {
|
||||
Rectangle()
|
||||
.frame(width: 12, height: 12)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 64.0)
|
||||
.task {
|
||||
context.send(viewAction: .loadRoomData(roomIdentifier: room.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lastMessageFont: Font {
|
||||
if room.hasUnreads {
|
||||
return .element.subheadline.bold()
|
||||
} else {
|
||||
return .element.subheadline
|
||||
}
|
||||
}
|
||||
|
||||
var lastMessageForegroundColor: Color {
|
||||
if room.hasUnreads {
|
||||
return .element.primaryContent
|
||||
} else {
|
||||
return .element.secondaryContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeScreenRoomCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
.tint(.element.accent)
|
||||
body.preferredColorScheme(.dark)
|
||||
.tint(.element.accent)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
|
||||
return VStack {
|
||||
ForEach(viewModel.context.viewState.rooms) { room in
|
||||
HomeScreenRoomCell(room: room, context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,6 @@ struct RoomScreenCoordinatorParameters {
|
||||
let timelineController: RoomTimelineControllerProtocol
|
||||
let roomName: String?
|
||||
let roomAvatar: UIImage?
|
||||
let roomEncryptionBadge: UIImage?
|
||||
}
|
||||
|
||||
final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
@ -45,8 +44,7 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
let viewModel = RoomScreenViewModel(timelineController: parameters.timelineController,
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
roomName: parameters.roomName,
|
||||
roomAvatar: parameters.roomAvatar,
|
||||
roomEncryptionBadge: parameters.roomEncryptionBadge)
|
||||
roomAvatar: parameters.roomAvatar)
|
||||
|
||||
let view = RoomScreen(context: viewModel.context)
|
||||
roomScreenViewModel = viewModel
|
||||
|
@ -43,6 +43,7 @@ enum RoomScreenViewAction {
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
var roomId: String
|
||||
var roomTitle = ""
|
||||
var roomAvatar: UIImage?
|
||||
var roomEncryptionBadge: UIImage?
|
||||
@ -54,7 +55,6 @@ struct RoomScreenViewState: BindableState {
|
||||
|
||||
var composerMode: RoomScreenComposerMode = .default
|
||||
|
||||
var messageComposerDisabled = false // Remove this when we have local echoes
|
||||
var sendButtonDisabled: Bool {
|
||||
bindings.composerText.count == 0
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, Roo
|
||||
|
||||
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
|
||||
private enum Constants {
|
||||
static let backPaginationPageSize: UInt = 30
|
||||
static let backPaginationPageSize: UInt = 50
|
||||
}
|
||||
|
||||
private let timelineController: RoomTimelineControllerProtocol
|
||||
@ -36,7 +36,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
self.timelineController = timelineController
|
||||
self.timelineViewFactory = timelineViewFactory
|
||||
|
||||
super.init(initialViewState: RoomScreenViewState(roomTitle: roomName ?? "Unknown room 💥",
|
||||
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomId,
|
||||
roomTitle: roomName ?? "Unknown room 💥",
|
||||
roomAvatar: roomAvatar,
|
||||
roomEncryptionBadge: roomEncryptionBadge,
|
||||
bindings: .init(composerText: "", composerFocused: false)))
|
||||
@ -106,19 +107,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
fatalError("This message should never be empty")
|
||||
}
|
||||
|
||||
state.messageComposerDisabled = true
|
||||
|
||||
switch state.composerMode {
|
||||
case .reply(let itemId, _):
|
||||
await timelineController.sendReply(state.bindings.composerText, to: itemId)
|
||||
default:
|
||||
await timelineController.sendMessage(state.bindings.composerText)
|
||||
}
|
||||
let message = state.bindings.composerText
|
||||
|
||||
state.bindings.composerText = ""
|
||||
state.composerMode = .default
|
||||
|
||||
state.messageComposerDisabled = false
|
||||
switch state.composerMode {
|
||||
case .reply(let itemId, _):
|
||||
await timelineController.sendReply(message, to: itemId)
|
||||
default:
|
||||
await timelineController.sendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private func displayError(_ type: RoomScreenErrorType) {
|
||||
@ -144,14 +143,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
return []
|
||||
}
|
||||
|
||||
let actions: [TimelineItemContextMenuAction] = [
|
||||
var actions: [TimelineItemContextMenuAction] = [
|
||||
.copy, .quote, .copyPermalink, .reply
|
||||
]
|
||||
|
||||
#warning("Outgoing actions to be handled with the new Timeline API.")
|
||||
// if timelineItem.isOutgoing {
|
||||
// actions.append(.redact)
|
||||
// }
|
||||
if let item = timelineItem as? EventBasedTimelineItemProtocol, item.isOutgoing {
|
||||
actions.append(.redact)
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
@ -26,46 +26,48 @@ struct MessageComposer: View {
|
||||
let replyCancellationAction: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom) {
|
||||
let rect = RoundedRectangle(cornerRadius: 8.0)
|
||||
VStack(alignment: .leading, spacing: 2.0) {
|
||||
if case let .reply(_, displayName) = type {
|
||||
MessageComposerReplyHeader(displayName: displayName, action: replyCancellationAction)
|
||||
}
|
||||
let rect = RoundedRectangle(cornerRadius: borderRadius)
|
||||
VStack(alignment: .leading, spacing: 4.0) {
|
||||
if case let .reply(_, displayName) = type {
|
||||
MessageComposerReplyHeader(displayName: displayName, action: replyCancellationAction)
|
||||
}
|
||||
HStack(alignment: .center) {
|
||||
MessageComposerTextField(placeholder: "Send a message",
|
||||
text: $text,
|
||||
focused: $focused,
|
||||
maxHeight: 300)
|
||||
|
||||
Button {
|
||||
sendAction()
|
||||
} label: {
|
||||
Image(systemName: "paperplane")
|
||||
.font(.element.title3)
|
||||
.foregroundColor(sendingDisabled ? .element.tempActionBackground : .element.tempActionForeground)
|
||||
.padding(8.0)
|
||||
.background(
|
||||
Circle()
|
||||
.foregroundColor(sendingDisabled ? .clear : .element.tempActionBackground)
|
||||
)
|
||||
}
|
||||
.disabled(sendingDisabled)
|
||||
.animation(.elementDefault, value: sendingDisabled)
|
||||
.keyboardShortcut(.return, modifiers: [.command])
|
||||
.padding(4.0)
|
||||
}
|
||||
.padding(4.0)
|
||||
.frame(minHeight: 44.0)
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(borderColor, lineWidth: borderWidth))
|
||||
.animation(.elementDefault, value: type)
|
||||
.animation(.elementDefault, value: borderWidth)
|
||||
|
||||
Button {
|
||||
sendAction()
|
||||
} label: {
|
||||
Image(uiImage: Asset.Images.timelineComposerSendMessage.image)
|
||||
.background(Circle()
|
||||
.foregroundColor(.global.white)
|
||||
)
|
||||
}
|
||||
.padding(.bottom, 6.0)
|
||||
.disabled(sendingDisabled)
|
||||
.opacity(sendingDisabled ? 0.5 : 1.0)
|
||||
.animation(.elementDefault, value: sendingDisabled)
|
||||
.keyboardShortcut(.return, modifiers: [.command])
|
||||
}
|
||||
.padding(.leading, 12.0)
|
||||
.background(.thinMaterial)
|
||||
.clipShape(rect)
|
||||
.animation(.elementDefault, value: type)
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
.element.accent
|
||||
}
|
||||
|
||||
private var borderWidth: CGFloat {
|
||||
focused ? 2.0 : 1.0
|
||||
private var borderRadius: CGFloat {
|
||||
switch type {
|
||||
case .default:
|
||||
return 28.0
|
||||
case .reply:
|
||||
return 12.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,8 +85,9 @@ private struct MessageComposerReplyHeader: View {
|
||||
Button {
|
||||
action()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.element.caption2)
|
||||
Image(systemName: "x.circle")
|
||||
.font(.element.callout)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(4.0)
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
let textView = UITextView()
|
||||
textView.delegate = context.coordinator
|
||||
|
||||
textView.textColor = .element.primaryContent
|
||||
textView.isEditable = true
|
||||
textView.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
textView.isSelectable = true
|
||||
@ -92,9 +93,8 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
UITextViewWrapper.recalculateHeight(view: textView, result: $calculatedHeight, maxHeight: maxHeight)
|
||||
|
||||
if focused, textView.window != nil, !textView.isFirstResponder {
|
||||
// Avoid cycle detected through attribute warnings
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async { // Avoid cycle detected through attribute warnings
|
||||
if focused, textView.window != nil, !textView.isFirstResponder {
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,8 @@ struct RoomHeaderView: View {
|
||||
.scaledToFill()
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.roomTitle)
|
||||
PlaceholderAvatarImage(text: context.viewState.roomTitle,
|
||||
contentId: context.viewState.roomId)
|
||||
.accessibilityIdentifier("roomAvatarPlaceholderImage")
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ struct RoomScreen: View {
|
||||
context.send(viewAction: .cancelReply)
|
||||
}
|
||||
.padding()
|
||||
.opacity(context.viewState.messageComposerDisabled ? 0.5 : 1.0)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
@ -24,25 +24,30 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ScaledMetric private var minBubbleWidth = 44
|
||||
@Environment(\.timelineWidth) private var timelineWidth
|
||||
@ScaledMetric private var senderNameVerticalPadding = 3
|
||||
private let bubbleWidthPercentIncoming = 0.72 // 281/390
|
||||
private let bubbleWidthPercentOutgoing = 0.68 // 267/390
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: alignment, spacing: -5) {
|
||||
VStack(alignment: alignment, spacing: -12) {
|
||||
if !timelineItem.isOutgoing {
|
||||
header
|
||||
.zIndex(1)
|
||||
}
|
||||
if timelineItem.isOutgoing {
|
||||
HStack {
|
||||
Spacer()
|
||||
styledContentWithReactions
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.leading, 51)
|
||||
} else {
|
||||
styledContentWithReactions
|
||||
VStack(alignment: alignment) {
|
||||
if timelineItem.isOutgoing {
|
||||
HStack {
|
||||
Spacer()
|
||||
styledContentWithReactions
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.leading, 16)
|
||||
.padding(.trailing, 51)
|
||||
} else {
|
||||
styledContentWithReactions
|
||||
.padding(.leading, 24)
|
||||
.padding(.trailing, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,10 +61,10 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
TimelineSenderAvatarView(timelineItem: timelineItem)
|
||||
Text(timelineItem.senderDisplayName ?? timelineItem.senderId)
|
||||
.font(.body)
|
||||
.font(.element.footnoteBold)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
.padding(.vertical, senderNameVerticalPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,9 +78,10 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
if !timelineItem.properties.reactions.isEmpty {
|
||||
TimelineReactionsView(reactions: timelineItem.properties.reactions,
|
||||
alignment: alignment) { key in
|
||||
alignment: .leading) { key in
|
||||
context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id))
|
||||
}
|
||||
.frame(width: bubbleWidth - 24)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
@ -83,24 +89,27 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
@ViewBuilder
|
||||
var styledContent: some View {
|
||||
if timelineItem.isOutgoing {
|
||||
styledContentOutgoing
|
||||
} else {
|
||||
styledContentIncoming
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var styledContentOutgoing: some View {
|
||||
if timelineItem.inGroupState == .single || timelineItem.inGroupState == .beginning {
|
||||
Spacer()
|
||||
.frame(height: 8)
|
||||
}
|
||||
if shouldAvoidBubbling {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
content()
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
Text(timelineItem.timestamp)
|
||||
.foregroundColor(.global.white)
|
||||
.font(.element.caption2)
|
||||
.padding(4)
|
||||
.background(Color(white: 0, opacity: 0.7))
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
.offset(x: -8, y: -8)
|
||||
}
|
||||
content()
|
||||
.frame(width: bubbleWidth)
|
||||
.cornerRadius(12, inGroupState: timelineItem.inGroupState)
|
||||
} else {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
content()
|
||||
.frame(minWidth: minBubbleWidth, alignment: .leading)
|
||||
.frame(width: bubbleWidth - 24, alignment: .leading)
|
||||
|
||||
if timelineItem.properties.isEdited {
|
||||
Text(ElementL10n.editedSuffix)
|
||||
@ -108,25 +117,50 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||
.clipped()
|
||||
.background(bubbleColor)
|
||||
.cornerRadius(12)
|
||||
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||
.background(Color.element.systemGray5)
|
||||
.cornerRadius(12, inGroupState: timelineItem.inGroupState)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var styledContentIncoming: some View {
|
||||
if shouldAvoidBubbling {
|
||||
content()
|
||||
.frame(width: bubbleWidth)
|
||||
.cornerRadius(12, inGroupState: timelineItem.inGroupState)
|
||||
} else {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
content()
|
||||
.frame(width: bubbleWidth - 24, alignment: .leading)
|
||||
|
||||
if timelineItem.properties.isEdited {
|
||||
Text(ElementL10n.editedSuffix)
|
||||
.font(.element.caption2)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||
.background(Color.element.systemGray6) // Demo time!
|
||||
.cornerRadius(12, inGroupState: timelineItem.inGroupState) // Demo time!
|
||||
// .overlay(
|
||||
// RoundedCorner(radius: 18, inGroupState: timelineItem.inGroupState)
|
||||
// .stroke(Color.element.systemGray5)
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldAvoidBubbling: Bool {
|
||||
timelineItem is ImageRoomTimelineItem
|
||||
}
|
||||
|
||||
private var bubbleColor: Color {
|
||||
let opacity = colorScheme == .light ? 0.06 : 0.15
|
||||
return timelineItem.isOutgoing ? .element.accent.opacity(opacity) : .element.system
|
||||
}
|
||||
|
||||
private var alignment: HorizontalAlignment {
|
||||
timelineItem.isOutgoing ? .trailing : .leading
|
||||
}
|
||||
|
||||
private var bubbleWidth: CGFloat {
|
||||
timelineWidth * (timelineItem.isOutgoing ? bubbleWidthPercentOutgoing : bubbleWidthPercentIncoming)
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
||||
@ -144,6 +178,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
.timelineStyle(.bubbles)
|
||||
.timelineWidth(390)
|
||||
.padding(.horizontal, 8)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
@ -50,3 +50,22 @@ extension View {
|
||||
environment(\.timelineStyle, style)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline Width
|
||||
|
||||
private struct TimelineWidthKey: EnvironmentKey {
|
||||
static let defaultValue: CGFloat = 0
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var timelineWidth: CGFloat {
|
||||
get { self[TimelineWidthKey.self] }
|
||||
set { self[TimelineWidthKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func timelineWidth(_ width: CGFloat) -> some View {
|
||||
environment(\.timelineWidth, width)
|
||||
}
|
||||
}
|
||||
|
@ -25,9 +25,9 @@ struct EmoteRoomTimelineView: View {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "face.dashed").padding(.top, 1.0)
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
FormattedBodyText(isOutgoing: timelineItem.isOutgoing, attributedComponents: attributedComponents)
|
||||
} else {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
FormattedBodyText(isOutgoing: timelineItem.isOutgoing, text: timelineItem.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,6 +59,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
|
@ -18,19 +18,32 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct FormattedBodyText: View {
|
||||
#warning("this is a dirty fix for demo, should be refactored after new timeline api")
|
||||
let isOutgoing: Bool
|
||||
let attributedComponents: [AttributedStringBuilderComponent]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
ForEach(attributedComponents, id: \.self) { component in
|
||||
if component.isBlockquote {
|
||||
HStack(spacing: 4.0) {
|
||||
Rectangle()
|
||||
.foregroundColor(Color.red)
|
||||
.frame(width: 4.0)
|
||||
if isOutgoing {
|
||||
Text(component.attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||
.clipped()
|
||||
.background(Color.element.systemGray4)
|
||||
.cornerRadius(13)
|
||||
} else {
|
||||
Text(component.attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12))
|
||||
.clipped()
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 13)
|
||||
.stroke(Color.element.systemGray5)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(component.attributedString)
|
||||
@ -44,7 +57,8 @@ struct FormattedBodyText: View {
|
||||
}
|
||||
|
||||
extension FormattedBodyText {
|
||||
init(text: String) {
|
||||
init(isOutgoing: Bool, text: String) {
|
||||
self.isOutgoing = isOutgoing
|
||||
attributedComponents = [.init(attributedString: AttributedString(text), isBlockquote: false)]
|
||||
}
|
||||
}
|
||||
@ -86,11 +100,11 @@ struct FormattedBodyText_Previews: PreviewProvider {
|
||||
let attributedString = attributedStringBuilder.fromHTML(htmlString)
|
||||
|
||||
if let components = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) {
|
||||
FormattedBodyText(attributedComponents: components)
|
||||
FormattedBodyText(isOutgoing: true, attributedComponents: components)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
FormattedBodyText(text: "Some plain text that's not an attributed component.")
|
||||
FormattedBodyText(isOutgoing: true, text: "Some plain text that's not an attributed component.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,39 +21,32 @@ struct ImageRoomTimelineView: View {
|
||||
let timelineItem: ImageRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
if timelineItem.image != nil || timelineItem.blurhash != nil { // Fixes view heights after loading finishes
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
if let aspectRatio = timelineItem.aspectRatio {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
.frame(maxHeight: 1000.0)
|
||||
} else {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
HStack {
|
||||
Spacer()
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView("Loading")
|
||||
Spacer()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
.frame(maxHeight: 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +63,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
text: "Some image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
@ -79,6 +73,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
text: "Some other image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
@ -88,6 +83,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
text: "Blurhashed image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
|
@ -25,9 +25,9 @@ struct NoticeRoomTimelineView: View {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "exclamationmark.bubble").padding(.top, 2.0)
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
FormattedBodyText(isOutgoing: timelineItem.isOutgoing, attributedComponents: attributedComponents)
|
||||
} else {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
FormattedBodyText(isOutgoing: timelineItem.isOutgoing, text: timelineItem.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -60,6 +60,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
|
@ -18,22 +18,32 @@ import SwiftUI
|
||||
|
||||
struct PlaceholderAvatarImage: View {
|
||||
private let textForImage: String
|
||||
private let contentId: String?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.element.accent
|
||||
bgColor
|
||||
Text(textForImage)
|
||||
.padding(4)
|
||||
.foregroundColor(.white)
|
||||
// Make the text resizable (i.e. Make it large and then allow it to scale down)
|
||||
.font(.system(size: 200).weight(.semibold))
|
||||
.minimumScaleFactor(0.001)
|
||||
.foregroundColor(.element.background)
|
||||
.font(.title2.bold())
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
}
|
||||
|
||||
init(text: String) {
|
||||
init(text: String, contentId: String? = nil) {
|
||||
textForImage = text.first?.uppercased() ?? ""
|
||||
self.contentId = contentId
|
||||
}
|
||||
|
||||
private var bgColor: Color {
|
||||
guard let contentId = contentId else {
|
||||
return .element.accent
|
||||
}
|
||||
|
||||
let colors = Color.element.contentAndAvatars
|
||||
let colorIndex = Int(contentId.hashCode % Int32(colors.count))
|
||||
return Color.element.contentAndAvatars[colorIndex % colors.count]
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +55,7 @@ struct PlaceholderAvatarImage_Previews: PreviewProvider {
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
PlaceholderAvatarImage(text: "X")
|
||||
PlaceholderAvatarImage(text: "X", contentId: "@userid:matrix.org")
|
||||
.clipShape(Circle())
|
||||
.frame(width: 150, height: 100)
|
||||
}
|
||||
|
@ -21,33 +21,12 @@ struct SeparatorRoomTimelineView: View {
|
||||
let timelineItem: SeparatorRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
LabelledDivider(label: timelineItem.text)
|
||||
Text(timelineItem.text)
|
||||
.font(.element.footnote)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.id(timelineItem.id)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
struct LabelledDivider: View {
|
||||
let label: String
|
||||
let color: Color
|
||||
|
||||
init(label: String, color: Color = Color.element.secondaryContent) {
|
||||
self.label = label
|
||||
self.color = color
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
line
|
||||
Text(label)
|
||||
.foregroundColor(color)
|
||||
.fixedSize()
|
||||
line
|
||||
}
|
||||
}
|
||||
|
||||
var line: some View {
|
||||
VStack { Divider().background(color) }
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,9 +23,9 @@ struct TextRoomTimelineView: View {
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
FormattedBodyText(isOutgoing: timelineItem.isOutgoing, attributedComponents: attributedComponents)
|
||||
} else {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
FormattedBodyText(isOutgoing: timelineItem.isOutgoing, text: timelineItem.text)
|
||||
}
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
@ -75,6 +75,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: shouldShowSenderDetails,
|
||||
inGroupState: .single,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: senderId)
|
||||
}
|
||||
|
@ -28,26 +28,26 @@ struct TimelineItemList: View {
|
||||
|
||||
let bottomVisiblePublisher: PassthroughSubject<Bool, Never>
|
||||
let scrollToBottomPublisher: PassthroughSubject<Void, Never>
|
||||
|
||||
|
||||
@State private var viewFrame: CGRect = .zero
|
||||
|
||||
var body: some View {
|
||||
// The observer behaves differently when not in an reader
|
||||
ScrollViewReader { _ in
|
||||
List {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.opacity(context.viewState.isBackPaginating ? 1.0 : 0.0)
|
||||
.animation(.elementDefault, value: context.viewState.isBackPaginating)
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity)
|
||||
.opacity(context.viewState.isBackPaginating ? 1.0 : 0.0)
|
||||
.animation(.elementDefault, value: context.viewState.isBackPaginating)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
// No idea why previews don't work otherwise
|
||||
ForEach(isPreview ? context.viewState.items : timelineItems) { timelineItem in
|
||||
timelineItem
|
||||
.contextMenu(menuItems: {
|
||||
.contextMenu {
|
||||
context.viewState.contextMenuBuilder?(timelineItem.id)
|
||||
})
|
||||
}
|
||||
.opacity(opacityForItem(timelineItem))
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
@ -65,6 +65,8 @@ struct TimelineItemList: View {
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.background(ViewFrameReader(frame: $viewFrame))
|
||||
.environment(\.timelineWidth, viewFrame.width)
|
||||
.timelineStyle(settings.timelineStyle)
|
||||
.environment(\.defaultMinListRowHeight, 0.0)
|
||||
.introspectTableView { tableView in
|
||||
@ -81,24 +83,24 @@ struct TimelineItemList: View {
|
||||
// Check if there are enough items. Otherwise ask for more
|
||||
attemptBackPagination()
|
||||
}
|
||||
.onAppear(perform: {
|
||||
.onAppear {
|
||||
if timelineItems != context.viewState.items {
|
||||
timelineItems = context.viewState.items
|
||||
}
|
||||
})
|
||||
.onReceive(scrollToBottomPublisher, perform: {
|
||||
}
|
||||
.onReceive(scrollToBottomPublisher) {
|
||||
tableViewObserver.scrollToBottom(animated: true)
|
||||
})
|
||||
.onReceive(tableViewObserver.scrollViewTopVisiblePublisher, perform: { isTopVisible in
|
||||
}
|
||||
.onReceive(tableViewObserver.scrollViewTopVisiblePublisher) { isTopVisible in
|
||||
if !isTopVisible || context.viewState.isBackPaginating {
|
||||
return
|
||||
}
|
||||
|
||||
attemptBackPagination()
|
||||
})
|
||||
.onReceive(tableViewObserver.scrollViewBottomVisiblePublisher, perform: { isBottomVisible in
|
||||
}
|
||||
.onReceive(tableViewObserver.scrollViewBottomVisiblePublisher) { isBottomVisible in
|
||||
bottomVisiblePublisher.send(isBottomVisible)
|
||||
})
|
||||
}
|
||||
.onChange(of: context.viewState.items) { _ in
|
||||
// Don't update the list while moving
|
||||
if tableViewObserver.isDecelerating || tableViewObserver.isTracking {
|
||||
@ -109,7 +111,7 @@ struct TimelineItemList: View {
|
||||
tableViewObserver.saveCurrentOffset()
|
||||
timelineItems = context.viewState.items
|
||||
}
|
||||
.onReceive(tableViewObserver.scrollViewDidRestPublisher, perform: {
|
||||
.onReceive(tableViewObserver.scrollViewDidRestPublisher) {
|
||||
if hasPendingChanges == false {
|
||||
return
|
||||
}
|
||||
@ -117,13 +119,13 @@ struct TimelineItemList: View {
|
||||
tableViewObserver.saveCurrentOffset()
|
||||
timelineItems = context.viewState.items
|
||||
hasPendingChanges = false
|
||||
})
|
||||
.onChange(of: timelineItems, perform: { _ in
|
||||
}
|
||||
.onChange(of: timelineItems) { _ in
|
||||
tableViewObserver.restoreSavedOffset()
|
||||
|
||||
// Check if there are enough items. Otherwise ask for more
|
||||
attemptBackPagination()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ import SwiftUI
|
||||
struct TimelineSenderAvatarView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
@ScaledMetric private var avatarSize = 26
|
||||
@ScaledMetric private var avatarSize = 32
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
@ -30,14 +30,15 @@ struct TimelineSenderAvatarView: View {
|
||||
.scaledToFill()
|
||||
.overlay(Circle().stroke(Color.element.accent))
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: timelineItem.senderDisplayName ?? timelineItem.senderId)
|
||||
PlaceholderAvatarImage(text: timelineItem.senderDisplayName ?? timelineItem.senderId,
|
||||
contentId: timelineItem.senderId)
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.element.background, lineWidth: 2)
|
||||
.stroke(Color.element.background, lineWidth: 3)
|
||||
)
|
||||
|
||||
.animation(.elementDefault, value: timelineItem.senderAvatar)
|
||||
|
@ -27,12 +27,34 @@ enum SessionVerificationViewModelAction {
|
||||
struct SessionVerificationViewState: BindableState {
|
||||
var verificationState: SessionVerificationStateMachine.State = .initial
|
||||
|
||||
var shouldDisableDismissButton: Bool {
|
||||
verificationState != .verified
|
||||
var title: String? {
|
||||
switch verificationState {
|
||||
case .showingChallenge:
|
||||
return ElementL10n.sessionVerificationScreenEmojisTitle
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var shouldDisableCancelButton: Bool {
|
||||
verificationState == .verified
|
||||
var message: String {
|
||||
switch verificationState {
|
||||
case .initial:
|
||||
return ElementL10n.verificationOpenOtherToVerify
|
||||
case .requestingVerification:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .acceptingChallenge:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .decliningChallenge:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .cancelling:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .showingChallenge:
|
||||
return ElementL10n.sessionVerificationScreenEmojisMessage
|
||||
case .verified:
|
||||
return ElementL10n.verificationConclusionOkSelfNotice
|
||||
case .cancelled:
|
||||
return ElementL10n.verificationCancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +63,5 @@ enum SessionVerificationViewAction {
|
||||
case restart
|
||||
case accept
|
||||
case decline
|
||||
case dismiss
|
||||
case cancel
|
||||
case close
|
||||
}
|
||||
|
@ -66,22 +66,10 @@ class SessionVerificationStateMachine {
|
||||
stateMachine.state
|
||||
}
|
||||
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
init() {
|
||||
stateMachine = StateMachine(state: .initial) { machine in
|
||||
machine.addRoutes(event: .requestVerification, transitions: [.initial => .requestingVerification])
|
||||
machine.addRoutes(event: .didFail, transitions: [.requestingVerification => .initial])
|
||||
|
||||
machine.addRoutes(event: .cancel, transitions: [.requestingVerification => .cancelling])
|
||||
machine.addRoutes(event: .didCancel, transitions: [.requestingVerification => .cancelled])
|
||||
|
||||
// Cancellation request from the other party should either take us from `.cancelling`
|
||||
// to `.cancelled` or keep us in `.cancelled` if already there. There is more `.didCancel`
|
||||
// handling in `addRouteMapping` for states containing associated values
|
||||
machine.addRoutes(event: .didCancel, transitions: [.cancelling => .cancelled])
|
||||
machine.addRoutes(event: .didCancel, transitions: [.cancelled => .cancelled])
|
||||
machine.addRoutes(event: .didFail, transitions: [.cancelled => .cancelled])
|
||||
|
||||
machine.addRoutes(event: .restart, transitions: [.cancelled => .initial])
|
||||
|
||||
// Transitions with associated values need to be handled through `addRouteMapping`
|
||||
@ -103,18 +91,9 @@ class SessionVerificationStateMachine {
|
||||
case (.didFail, .decliningChallenge(let emojis)):
|
||||
return .showingChallenge(emojis: emojis)
|
||||
|
||||
case (.cancel, .showingChallenge):
|
||||
case (.cancel, _):
|
||||
return .cancelling
|
||||
case (.cancel, .acceptingChallenge):
|
||||
return .cancelling
|
||||
case (.cancel, .decliningChallenge):
|
||||
return .cancelling
|
||||
|
||||
case (.didCancel, .showingChallenge):
|
||||
return .cancelled
|
||||
case (.didCancel, .acceptingChallenge):
|
||||
return .cancelled
|
||||
case (.didCancel, .decliningChallenge):
|
||||
case (.didCancel, _):
|
||||
return .cancelled
|
||||
|
||||
default:
|
||||
@ -123,8 +102,6 @@ class SessionVerificationStateMachine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable cyclomatic_complexity
|
||||
|
||||
/// Attempt to move the state machine to another state through an event
|
||||
/// It will either invoke the `transitionHandler` or the `errorHandler` depending on its current state
|
||||
|
@ -74,9 +74,7 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer
|
||||
stateMachine.processEvent(.requestVerification)
|
||||
case .restart:
|
||||
stateMachine.processEvent(.restart)
|
||||
case .dismiss:
|
||||
callback?(.finished)
|
||||
case .cancel:
|
||||
case .close:
|
||||
guard stateMachine.state == .initial ||
|
||||
stateMachine.state == .verified ||
|
||||
stateMachine.state == .cancelled else {
|
||||
@ -115,7 +113,7 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer
|
||||
}
|
||||
|
||||
stateMachine.addErrorHandler { context in
|
||||
fatalError("Failed transition with context: \(context)")
|
||||
MXLog.error("Failed transition with context: \(context)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct SessionVerificationScreen: View {
|
||||
@ -24,127 +23,132 @@ struct SessionVerificationScreen: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 32.0) {
|
||||
Text(heading)
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
switch context.viewState.verificationState {
|
||||
case .initial:
|
||||
StateIcon(systemName: "lock.shield")
|
||||
Button(ElementL10n.startVerification) {
|
||||
context.send(viewAction: .start)
|
||||
}
|
||||
.buttonStyle(.elementAction(.regular))
|
||||
.accessibilityIdentifier("startButton")
|
||||
|
||||
case .cancelled:
|
||||
StateIcon(systemName: "xmark.shield")
|
||||
.accessibilityIdentifier("sessionVerificationFailedIcon")
|
||||
|
||||
Button(ElementL10n.globalRetry) {
|
||||
context.send(viewAction: .restart)
|
||||
}
|
||||
.buttonStyle(.elementAction(.regular))
|
||||
.accessibilityIdentifier("restartButton")
|
||||
|
||||
case .requestingVerification:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("requestingVerificationProgressView")
|
||||
case .cancelling:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("cancellingVerificationProgressView")
|
||||
case .acceptingChallenge:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("acceptingChallengeProgressView")
|
||||
case .decliningChallenge:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("decliningChallengeProgressView")
|
||||
|
||||
case .showingChallenge(let emojis):
|
||||
HStack(spacing: 8.0) {
|
||||
ForEach(emojis.prefix(4), id: \.self) { emoji in
|
||||
EmojiView(emoji: emoji)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 8.0) {
|
||||
ForEach(emojis.suffix(from: 4), id: \.self) { emoji in
|
||||
EmojiView(emoji: emoji)
|
||||
}
|
||||
ScrollView {
|
||||
VStack(spacing: 32.0) {
|
||||
if let title = context.viewState.title {
|
||||
Text(title)
|
||||
.font(.element.headlineBold)
|
||||
.foregroundColor(.element.systemPrimaryLabel)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
actionButtons
|
||||
case .verified:
|
||||
StateIcon(systemName: "checkmark.shield")
|
||||
.accessibilityIdentifier("sessionVerificationSucceededIcon")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.padding(.top, 64)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(ElementL10n.verificationVerifyDevice)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(ElementL10n.done) {
|
||||
context.send(viewAction: .dismiss)
|
||||
}
|
||||
.disabled(context.viewState.shouldDisableDismissButton)
|
||||
.accessibilityIdentifier("dismissButton")
|
||||
}
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(ElementL10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
.disabled(context.viewState.shouldDisableCancelButton)
|
||||
.accessibilityIdentifier("cancelButton")
|
||||
Text(context.viewState.message)
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.systemPrimaryLabel)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
mainContent
|
||||
}
|
||||
.padding()
|
||||
.padding(.top, 64)
|
||||
.frame(maxWidth: .infinity)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(ElementL10n.verificationProfileVerify)
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.background(Color.element.systemSecondaryBackground)
|
||||
.safeAreaInset(edge: .bottom) { actionButtons.padding() }
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var heading: String {
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
switch context.viewState.verificationState {
|
||||
case .initial:
|
||||
return ElementL10n.verificationOpenOtherToVerify
|
||||
case .requestingVerification:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .acceptingChallenge:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .decliningChallenge:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .cancelling:
|
||||
return ElementL10n.verificationRequestWaiting
|
||||
case .showingChallenge:
|
||||
return ElementL10n.verificationEmojiNotice
|
||||
case .verified:
|
||||
return ElementL10n.verificationConclusionOkSelfNotice
|
||||
StateIcon(systemName: "lock.shield")
|
||||
|
||||
case .cancelled:
|
||||
return ElementL10n.verificationCancelled
|
||||
StateIcon(systemName: "xmark.shield")
|
||||
.accessibilityIdentifier("sessionVerificationFailedIcon")
|
||||
|
||||
case .requestingVerification:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("requestingVerificationProgressView")
|
||||
case .cancelling:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("cancellingVerificationProgressView")
|
||||
case .acceptingChallenge:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("acceptingChallengeProgressView")
|
||||
case .decliningChallenge:
|
||||
ProgressView()
|
||||
.accessibilityIdentifier("decliningChallengeProgressView")
|
||||
|
||||
case .showingChallenge(let emojis):
|
||||
HStack(spacing: 16) {
|
||||
ForEach(emojis.prefix(4), id: \.self) { emoji in
|
||||
EmojiView(emoji: emoji)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 16) {
|
||||
ForEach(emojis.suffix(from: 4), id: \.self) { emoji in
|
||||
EmojiView(emoji: emoji)
|
||||
}
|
||||
}
|
||||
case .verified:
|
||||
StateIcon(systemName: "checkmark.shield")
|
||||
.accessibilityIdentifier("sessionVerificationSucceededIcon")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var actionButtons: some View {
|
||||
HStack(spacing: 16.0) {
|
||||
Button(ElementL10n.verificationSasDoNotMatch) {
|
||||
context.send(viewAction: .decline)
|
||||
switch context.viewState.verificationState {
|
||||
case .initial:
|
||||
Button(ElementL10n.startVerification) {
|
||||
context.send(viewAction: .start)
|
||||
}
|
||||
.buttonStyle(.elementAction(.regular, color: .red))
|
||||
.accessibilityLabel("challengeDeclineButton")
|
||||
|
||||
Button(ElementL10n.verificationSasMatch) {
|
||||
context.send(viewAction: .accept)
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityIdentifier("startButton")
|
||||
|
||||
case .cancelled:
|
||||
Button(ElementL10n.globalRetry) {
|
||||
context.send(viewAction: .restart)
|
||||
}
|
||||
.buttonStyle(.elementAction(.regular))
|
||||
.accessibilityLabel("challengeAcceptButton")
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityIdentifier("restartButton")
|
||||
|
||||
case .showingChallenge:
|
||||
VStack(spacing: 30) {
|
||||
Button { context.send(viewAction: .accept) } label: {
|
||||
Label(ElementL10n.actionMatch, systemImage: "checkmark")
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityLabel("challengeAcceptButton")
|
||||
|
||||
Button(ElementL10n.no) {
|
||||
context.send(viewAction: .decline)
|
||||
}
|
||||
.font(.element.bodyBold)
|
||||
.accessibilityLabel("challengeDeclineButton")
|
||||
}
|
||||
|
||||
case .verified:
|
||||
Button(ElementL10n.finish) {
|
||||
context.send(viewAction: .close)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityIdentifier("finishButton")
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbarContent: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button { context.send(viewAction: .close) } label: {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
.font(.element.bodyBold)
|
||||
.foregroundColor(.element.systemSecondaryLabel)
|
||||
.accessibilityIdentifier("closeButton")
|
||||
}
|
||||
.padding(32.0)
|
||||
}
|
||||
|
||||
struct EmojiView: View {
|
||||
@ -153,9 +157,10 @@ struct SessionVerificationScreen: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16.0) {
|
||||
Text(emoji.symbol)
|
||||
.font(.largeTitle)
|
||||
.font(.element.largeTitleBold)
|
||||
Text(emoji.description)
|
||||
.font(.body)
|
||||
.font(.element.caption2)
|
||||
.foregroundColor(.element.systemSecondaryLabel)
|
||||
}
|
||||
.padding(8.0)
|
||||
}
|
||||
@ -167,6 +172,7 @@ struct SessionVerificationScreen: View {
|
||||
var body: some View {
|
||||
Image(systemName: systemName)
|
||||
.resizable()
|
||||
.font(.element.body.weight(.light))
|
||||
.scaledToFit()
|
||||
.foregroundColor(.element.accent)
|
||||
.frame(width: 100, height: 100)
|
||||
@ -181,23 +187,24 @@ struct SessionVerification_Previews: PreviewProvider {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
Group {
|
||||
sessionVerificationScreen(state: .initial)
|
||||
sessionVerificationScreen(state: .requestingVerification)
|
||||
sessionVerificationScreen(state: .cancelled)
|
||||
|
||||
|
||||
sessionVerificationScreen(state: .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
|
||||
sessionVerificationScreen(state: .verified)
|
||||
}
|
||||
.tint(Color.element.accent)
|
||||
}
|
||||
|
||||
|
||||
static func sessionVerificationScreen(state: SessionVerificationStateMachine.State) -> some View {
|
||||
let viewModel = SessionVerificationViewModel(sessionVerificationControllerProxy: MockSessionVerificationControllerProxy(),
|
||||
initialState: SessionVerificationViewState(verificationState: state))
|
||||
|
||||
|
||||
return SessionVerificationScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ struct SettingsCoordinatorParameters {
|
||||
}
|
||||
|
||||
enum SettingsCoordinatorAction {
|
||||
case dismiss
|
||||
case logout
|
||||
}
|
||||
|
||||
@ -38,6 +39,8 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var statusIndicator: UserIndicator?
|
||||
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ -50,7 +53,7 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
init(parameters: SettingsCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = SettingsViewModel()
|
||||
let viewModel = SettingsViewModel(withUserSession: parameters.userSession)
|
||||
let view = SettingsScreen(context: viewModel.context)
|
||||
settingsViewModel = viewModel
|
||||
settingsHostingController = UIHostingController(rootView: view)
|
||||
@ -61,6 +64,8 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("SettingsViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
case .close:
|
||||
self.callback?(.dismiss)
|
||||
case .toggleAnalytics:
|
||||
self.toggleAnalytics()
|
||||
case .reportBug:
|
||||
@ -68,7 +73,7 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
case .crash:
|
||||
self.parameters.bugReportService.crash()
|
||||
case .logout:
|
||||
self.callback?(.logout)
|
||||
self.confirmSignOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,12 +111,25 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
parameters.navigationRouter.push(coordinator, animated: true) { [weak self] in
|
||||
navigationRouter.push(coordinator, animated: true) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
private func confirmSignOut() {
|
||||
let alert = UIAlertController(title: ElementL10n.actionSignOut,
|
||||
message: ElementL10n.actionSignOutConfirmationSimple,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { [weak self] _ in
|
||||
self?.callback?(.logout)
|
||||
})
|
||||
|
||||
navigationRouter.present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
|
@ -15,12 +15,14 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum SettingsViewModelAction {
|
||||
case close
|
||||
case toggleAnalytics
|
||||
case reportBug
|
||||
case crash
|
||||
@ -31,6 +33,9 @@ enum SettingsViewModelAction {
|
||||
|
||||
struct SettingsViewState: BindableState {
|
||||
var bindings: SettingsViewStateBindings
|
||||
var userID: String
|
||||
var userAvatar: UIImage?
|
||||
var userDisplayName: String?
|
||||
}
|
||||
|
||||
struct SettingsViewStateBindings {
|
||||
@ -38,6 +43,7 @@ struct SettingsViewStateBindings {
|
||||
}
|
||||
|
||||
enum SettingsViewAction {
|
||||
case close
|
||||
case toggleAnalytics
|
||||
case reportBug
|
||||
case crash
|
||||
|
@ -25,21 +25,38 @@ class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let userSession: UserSessionProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((SettingsViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init() {
|
||||
init(withUserSession userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
let bindings = SettingsViewStateBindings()
|
||||
super.init(initialViewState: .init(bindings: bindings))
|
||||
super.init(initialViewState: .init(bindings: bindings, userID: userSession.userID))
|
||||
|
||||
Task {
|
||||
if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() {
|
||||
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, size: MediaProviderDefaultAvatarSize) {
|
||||
state.userAvatar = avatar
|
||||
}
|
||||
}
|
||||
|
||||
if case let .success(userDisplayName) = await self.userSession.clientProxy.loadUserDisplayName() {
|
||||
state.userDisplayName = userDisplayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: SettingsViewAction) async {
|
||||
switch viewAction {
|
||||
case .close:
|
||||
callback?(.close)
|
||||
case .toggleAnalytics:
|
||||
callback?(.toggleAnalytics)
|
||||
case .reportBug:
|
||||
|
@ -19,9 +19,12 @@ import SwiftUI
|
||||
struct SettingsScreen: View {
|
||||
// MARK: Private
|
||||
|
||||
@State private var showingLogoutConfirmation = false
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ObservedObject private var settings = ElementSettings.shared
|
||||
|
||||
@ScaledMetric private var avatarSize = 60.0
|
||||
@ScaledMetric private var menuIconSize = 30.0
|
||||
private let listRowInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ -31,6 +34,12 @@ struct SettingsScreen: View {
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
userSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
appearanceSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
analyticsSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
@ -44,7 +53,13 @@ struct SettingsScreen: View {
|
||||
tableView.backgroundColor = .clear
|
||||
}
|
||||
.navigationTitle(ElementL10n.settings)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.background(backgroundColor, ignoresSafeAreaEdges: .all)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
closeButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var versionText: some View {
|
||||
@ -52,25 +67,85 @@ struct SettingsScreen: View {
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
colorScheme == .light ? .element.system : .element.background
|
||||
.element.systemGray6
|
||||
}
|
||||
|
||||
private var rowBackgroundColor: Color {
|
||||
colorScheme == .light ? .element.background : .element.system
|
||||
}
|
||||
|
||||
private var userSection: some View {
|
||||
Section {
|
||||
HStack(spacing: 13) {
|
||||
userAvatar
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(context.viewState.userDisplayName ?? "")
|
||||
.font(.title3)
|
||||
Text(context.viewState.userID)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
}
|
||||
}
|
||||
|
||||
private var appearanceSection: some View {
|
||||
Section {
|
||||
Button(action: appearance) {
|
||||
HStack {
|
||||
Image(systemName: "paintpalette")
|
||||
.foregroundColor(.element.systemGray)
|
||||
.padding(4)
|
||||
.background(Color.element.systemGray6)
|
||||
.clipShape(Circle())
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
Text(ElementL10n.settingsAppearance)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.forward")
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("appearanceButton")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userAvatar: some View {
|
||||
if let avatar = context.viewState.userAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.userDisplayName ?? context.viewState.userID, contentId: context.viewState.userID)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
}
|
||||
}
|
||||
|
||||
private var analyticsSection: some View {
|
||||
Section(ElementL10n.settingsAnalytics) {
|
||||
Section {
|
||||
Button { context.send(viewAction: .reportBug) } label: {
|
||||
Text(ElementL10n.sendBugReport)
|
||||
HStack {
|
||||
Text(ElementL10n.sendBugReport)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.forward")
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.listRowSeparator(.hidden)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("reportBugButton")
|
||||
|
||||
if BuildSettings.settingsCrashButtonVisible {
|
||||
Button("Crash the app",
|
||||
Button("Crash app",
|
||||
role: .destructive) { context.send(viewAction: .crash)
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.accessibilityIdentifier("crashButton")
|
||||
}
|
||||
}
|
||||
@ -79,13 +154,14 @@ struct SettingsScreen: View {
|
||||
@ViewBuilder
|
||||
private var userInterfaceSection: some View {
|
||||
if BuildSettings.settingsShowTimelineStyle {
|
||||
Section(ElementL10n.settingsUserInterface) {
|
||||
Section {
|
||||
Picker(ElementL10n.settingsTimelineStyle, selection: $settings.timelineStyle) {
|
||||
ForEach(TimelineStyle.allCases, id: \.self) { style in
|
||||
Text(style.description)
|
||||
.tag(style)
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.accessibilityIdentifier("timelineStylePicker")
|
||||
}
|
||||
}
|
||||
@ -93,23 +169,52 @@ struct SettingsScreen: View {
|
||||
|
||||
private var logoutSection: some View {
|
||||
Section {
|
||||
Button { showingLogoutConfirmation = true } label: {
|
||||
Text(ElementL10n.actionSignOut)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("logoutButton")
|
||||
.confirmationDialog(ElementL10n.actionSignOutConfirmationSimple,
|
||||
isPresented: $showingLogoutConfirmation,
|
||||
titleVisibility: .visible) {
|
||||
Button(ElementL10n.actionSignOut,
|
||||
role: .destructive) { context.send(viewAction: .logout)
|
||||
Button(action: logout) {
|
||||
HStack {
|
||||
Image(systemName: "rectangle.portrait.and.arrow.right")
|
||||
.foregroundColor(.element.systemGray)
|
||||
.padding(4)
|
||||
.background(Color.element.systemGray6)
|
||||
.clipShape(Circle())
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
Text(ElementL10n.actionSignOut)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.forward")
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("logoutButton")
|
||||
} footer: {
|
||||
versionText
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button(action: close) {
|
||||
HStack {
|
||||
Image(systemName: "xmark")
|
||||
.font(.title3.bold())
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier("closeButton")
|
||||
}
|
||||
|
||||
private func appearance() {
|
||||
#warning("Not implemented")
|
||||
}
|
||||
|
||||
private func close() {
|
||||
context.send(viewAction: .close)
|
||||
}
|
||||
|
||||
private func logout() {
|
||||
context.send(viewAction: .logout)
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineStyle: CustomStringConvertible {
|
||||
@ -133,8 +238,13 @@ struct Settings_Previews: PreviewProvider {
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
let viewModel = SettingsViewModel()
|
||||
SettingsScreen(context: viewModel.context)
|
||||
.previewInterfaceOrientation(.portrait)
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
let viewModel = SettingsViewModel(withUserSession: userSession)
|
||||
|
||||
return NavigationView {
|
||||
SettingsScreen(context: viewModel.context)
|
||||
.previewInterfaceOrientation(.portrait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,13 +108,10 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
|
||||
|
||||
let loginTask: Task<Client, Error> = Task.detached {
|
||||
#warning("Use new api on next SDK release.")
|
||||
return try self.authenticationService.login(username: username,
|
||||
password: password)
|
||||
// try self.authenticationService.login(username: username,
|
||||
// password: password,
|
||||
// initialDeviceName: initialDeviceName,
|
||||
// deviceId: deviceId)
|
||||
try self.authenticationService.login(username: username,
|
||||
password: password,
|
||||
initialDeviceName: initialDeviceName,
|
||||
deviceId: deviceId)
|
||||
}
|
||||
|
||||
switch await loginTask.result {
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import Foundation
|
||||
import GZIP
|
||||
import MatrixRustSDK
|
||||
import Sentry
|
||||
import UIKit
|
||||
|
||||
|
@ -19,64 +19,91 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
private class WeakClientProxyWrapper: ClientDelegate {
|
||||
private class WeakClientProxyWrapper: ClientDelegate, SlidingSyncObserver {
|
||||
private weak var clientProxy: ClientProxy?
|
||||
|
||||
init(clientProxy: ClientProxy) {
|
||||
self.clientProxy = clientProxy
|
||||
}
|
||||
|
||||
func didReceiveSyncUpdate() {
|
||||
clientProxy?.didReceiveSyncUpdate()
|
||||
}
|
||||
// MARK: - ClientDelegate
|
||||
|
||||
func didReceiveSyncUpdate() { }
|
||||
|
||||
func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
|
||||
Task {
|
||||
await clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
|
||||
}
|
||||
}
|
||||
|
||||
func didUpdateRestoreToken() {
|
||||
clientProxy?.didUpdateRestoreToken()
|
||||
Task {
|
||||
await clientProxy?.didUpdateRestoreToken()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncDelegate
|
||||
|
||||
func didReceiveSyncUpdate(summary: UpdateSummary) {
|
||||
Task {
|
||||
await self.clientProxy?.didReceiveSlidingSyncUpdate(summary: summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClientProxy: ClientProxyProtocol {
|
||||
/// The maximum number of timeline events required during a sync request.
|
||||
static let syncLimit: UInt16 = 20
|
||||
static let syncLimit: UInt16 = 50
|
||||
|
||||
private let client: Client
|
||||
private let client: ClientProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
||||
|
||||
private(set) var rooms: [RoomProxy] = [] {
|
||||
didSet {
|
||||
callbacks.send(.updatedRoomsList)
|
||||
}
|
||||
}
|
||||
private var slidingSyncObserverToken: StoppableSpawn?
|
||||
private let slidingSync: SlidingSync
|
||||
|
||||
private var roomProxies = [String: RoomProxyProtocol]()
|
||||
|
||||
let roomSummaryProvider: RoomSummaryProviderProtocol
|
||||
|
||||
deinit {
|
||||
// These need to be inlined instead of using stopSync()
|
||||
// as we can't call async methods safely from deinit
|
||||
client.setDelegate(delegate: nil)
|
||||
slidingSyncObserverToken?.cancel()
|
||||
slidingSync.setObserver(observer: nil)
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||
|
||||
init(client: Client,
|
||||
init(client: ClientProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.client = client
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
|
||||
do {
|
||||
let slidingSyncBuilder = try client.slidingSync().homeserver(url: BuildSettings.slidingSyncProxyBaseURL.absoluteString)
|
||||
|
||||
let slidingSyncView = try SlidingSyncViewBuilder()
|
||||
.timelineLimit(limit: 10)
|
||||
.requiredState(requiredState: [RequiredState(key: "m.room.avatar", value: "")])
|
||||
.name(name: "HomeScreenView")
|
||||
.syncMode(mode: .fullSync)
|
||||
.build()
|
||||
|
||||
slidingSync = try slidingSyncBuilder
|
||||
.addView(view: slidingSyncView)
|
||||
// .withCommonExtensions()
|
||||
.build()
|
||||
|
||||
roomSummaryProvider = RoomSummaryProvider(slidingSyncController: slidingSync,
|
||||
slidingSyncView: slidingSyncView,
|
||||
roomMessageFactory: RoomMessageFactory())
|
||||
} catch {
|
||||
fatalError("Failed configuring sliding sync")
|
||||
}
|
||||
|
||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||
|
||||
#warning("Use isSoftLogout() api on next SDK release.")
|
||||
Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
|
||||
client.startSync(timelineLimit: ClientProxy.syncLimit)
|
||||
|
||||
Task { await updateRooms() }
|
||||
// if !client.isSoftLogout() {
|
||||
// Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
|
||||
// client.startSync(timelineLimit: ClientProxy.syncLimit)
|
||||
//
|
||||
// Task { await updateRooms() }
|
||||
// }
|
||||
}
|
||||
|
||||
var userIdentifier: String {
|
||||
@ -89,9 +116,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
|
||||
var isSoftLogout: Bool {
|
||||
#warning("Use isSoftLogout() api on next SDK release.")
|
||||
return false
|
||||
// client.isSoftLogout()
|
||||
client.isSoftLogout()
|
||||
}
|
||||
|
||||
var deviceId: String? {
|
||||
@ -116,6 +141,46 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func startSync() {
|
||||
guard !client.isSoftLogout() else {
|
||||
return
|
||||
}
|
||||
|
||||
slidingSync.setObserver(observer: WeakClientProxyWrapper(clientProxy: self))
|
||||
slidingSyncObserverToken = slidingSync.sync()
|
||||
}
|
||||
|
||||
func stopSync() {
|
||||
client.setDelegate(delegate: nil)
|
||||
|
||||
slidingSyncObserverToken?.cancel()
|
||||
slidingSync.setObserver(observer: nil)
|
||||
}
|
||||
|
||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol? {
|
||||
if let roomProxy = roomProxies[identifier] {
|
||||
return roomProxy
|
||||
}
|
||||
|
||||
do {
|
||||
guard let slidingSyncRoom = try slidingSync.getRoom(roomId: identifier),
|
||||
let room = slidingSyncRoom.fullRoom() else {
|
||||
MXLog.error("Failed retrieving room with identifier: \(identifier)")
|
||||
return nil
|
||||
}
|
||||
|
||||
let roomProxy = RoomProxy(slidingSyncRoom: slidingSyncRoom,
|
||||
room: room,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
roomProxies[identifier] = roomProxy
|
||||
|
||||
return roomProxy
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving room with identifier: \(identifier)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||
await Task.detached {
|
||||
do {
|
||||
@ -152,9 +217,18 @@ class ClientProxy: ClientProxyProtocol {
|
||||
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
|
||||
let bytes = try client.getMediaContent(source: source)
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
|
||||
try await Task.detached {
|
||||
let bytes = try self.client.getMediaContent(source: source)
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}.value
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
|
||||
try await Task.detached {
|
||||
let bytes = try self.client.getMediaThumbnail(source: source, width: UInt64(width), height: UInt64(height))
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}.value
|
||||
}
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
||||
@ -171,8 +245,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
func logout() async {
|
||||
do {
|
||||
#warning("Use logout() api on next SDK release.")
|
||||
// try client.logout()
|
||||
try client.logout()
|
||||
} catch {
|
||||
MXLog.error("Failed logging out with error: \(error)")
|
||||
}
|
||||
@ -180,16 +253,6 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
fileprivate func didReceiveSyncUpdate() {
|
||||
Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update")
|
||||
|
||||
callbacks.send(.receivedSyncUpdate)
|
||||
|
||||
Task.detached {
|
||||
await self.updateRooms()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
||||
}
|
||||
@ -198,32 +261,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
callbacks.send(.updatedRestoreToken)
|
||||
}
|
||||
|
||||
private func updateRooms() async {
|
||||
var currentRooms = rooms
|
||||
Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms")
|
||||
let sdkRooms = client.rooms()
|
||||
Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) rooms")
|
||||
fileprivate func didReceiveSlidingSyncUpdate(summary: UpdateSummary) {
|
||||
roomSummaryProvider.updateRoomsWithIdentifiers(summary.rooms)
|
||||
|
||||
Benchmark.startTrackingForIdentifier("ProcessingRooms", message: "Started processing \(sdkRooms.count) rooms")
|
||||
let diff = sdkRooms.map { $0.id() }.difference(from: currentRooms.map(\.id))
|
||||
|
||||
for change in diff {
|
||||
switch change {
|
||||
case .insert(_, let id, _):
|
||||
guard let sdkRoom = sdkRooms.first(where: { $0.id() == id }) else {
|
||||
MXLog.error("Failed retrieving sdk room with id: \(id)")
|
||||
break
|
||||
}
|
||||
currentRooms.append(RoomProxy(room: sdkRoom,
|
||||
roomMessageFactory: RoomMessageFactory(),
|
||||
backgroundTaskService: backgroundTaskService))
|
||||
case .remove(_, let id, _):
|
||||
currentRooms.removeAll { $0.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
Benchmark.endTrackingForIdentifier("ProcessingRooms", message: "Finished processing \(sdkRooms.count) rooms")
|
||||
|
||||
rooms = currentRooms
|
||||
// callbacks.send(.receivedSyncUpdate)
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum ClientProxyCallback {
|
||||
case updatedRoomsList
|
||||
case receivedSyncUpdate
|
||||
case receivedAuthError(isSoftLogout: Bool)
|
||||
case updatedRestoreToken
|
||||
@ -34,6 +33,7 @@ enum ClientProxyError: Error {
|
||||
case failedLoadingMedia
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol ClientProxyProtocol {
|
||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||
|
||||
@ -47,7 +47,13 @@ protocol ClientProxyProtocol {
|
||||
|
||||
var restoreToken: String? { get }
|
||||
|
||||
var rooms: [RoomProxy] { get }
|
||||
var roomSummaryProvider: RoomSummaryProviderProtocol { get }
|
||||
|
||||
func startSync()
|
||||
|
||||
func stopSync()
|
||||
|
||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol?
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError>
|
||||
|
||||
@ -59,7 +65,9 @@ protocol ClientProxyProtocol {
|
||||
|
||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
|
||||
|
||||
|
@ -26,10 +26,18 @@ struct MockClientProxy: ClientProxyProtocol {
|
||||
let homeserver = ""
|
||||
let restoreToken: String? = nil
|
||||
|
||||
let rooms = [RoomProxy]()
|
||||
let roomSummaryProvider: RoomSummaryProviderProtocol = MockRoomSummaryProvider()
|
||||
|
||||
func startSync() { }
|
||||
|
||||
func stopSync() { }
|
||||
|
||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||
.failure(.failedRetrievingDisplayName)
|
||||
.success("User display name")
|
||||
}
|
||||
|
||||
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
|
||||
@ -48,7 +56,11 @@ struct MockClientProxy: ClientProxyProtocol {
|
||||
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
|
||||
throw ClientProxyError.failedLoadingMedia
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
|
||||
throw ClientProxyError.failedLoadingMedia
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,6 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private let processingQueue: DispatchQueue
|
||||
|
||||
init(clientProxy: ClientProxyProtocol,
|
||||
imageCache: Kingfisher.ImageCache,
|
||||
@ -29,31 +28,30 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
self.clientProxy = clientProxy
|
||||
self.imageCache = imageCache
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent)
|
||||
}
|
||||
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage? {
|
||||
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage? {
|
||||
guard let source = source else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil)
|
||||
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), size: size)
|
||||
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
|
||||
}
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage? {
|
||||
guard let urlString = urlString else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
|
||||
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
|
||||
func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source) {
|
||||
func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source, size: size) {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
@ -62,21 +60,30 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
loadImageBgTask?.stop()
|
||||
}
|
||||
|
||||
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), size: size)
|
||||
|
||||
return await Task.detached { () -> Result<UIImage, MediaProviderError> in
|
||||
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: source.underlyingSource.url()),
|
||||
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey),
|
||||
let image = cacheResult.image {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
do {
|
||||
let imageData = try clientProxy.loadMediaContentForSource(source.underlyingSource)
|
||||
let imageData = try await Task.detached { () -> Data in
|
||||
if let size = size {
|
||||
return try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(size.width), height: UInt(size.height))
|
||||
} else {
|
||||
return try await clientProxy.loadMediaContentForSource(source.underlyingSource)
|
||||
}
|
||||
|
||||
}.value
|
||||
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
MXLog.error("Invalid image data")
|
||||
return .failure(.invalidImageData)
|
||||
}
|
||||
|
||||
imageCache.store(image, forKey: source.underlyingSource.url())
|
||||
imageCache.store(image, forKey: cacheKey)
|
||||
|
||||
return .success(image)
|
||||
} catch {
|
||||
@ -86,6 +93,16 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func cacheKeyForURLString(_ urlString: String, size: CGSize?) -> String {
|
||||
if let size = size {
|
||||
return "\(urlString){\(size.width),\(size.height)}"
|
||||
} else {
|
||||
return urlString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ImageCache {
|
||||
|
@ -22,13 +22,33 @@ enum MediaProviderError: Error {
|
||||
case invalidImageData
|
||||
}
|
||||
|
||||
let MediaProviderDefaultAvatarSize = CGSize(width: 44.0, height: 44.0)
|
||||
|
||||
@MainActor
|
||||
protocol MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage?
|
||||
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage?
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError>
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage?
|
||||
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage?
|
||||
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError>
|
||||
@discardableResult func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
}
|
||||
|
||||
extension MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage? {
|
||||
imageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
imageFromURLString(urlString, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromURLString(urlString, size: nil)
|
||||
}
|
||||
}
|
||||
|
@ -18,19 +18,23 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MockMediaProvider: MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage? {
|
||||
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
.failure(.failedRetrievingImage)
|
||||
}
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
nil
|
||||
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage? {
|
||||
if urlString != nil {
|
||||
return UIImage(systemName: "photo")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
.failure(.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@MainActor
|
||||
class MemberDetailProviderManager {
|
||||
private var memberDetailProviders: [String: MemberDetailProviderProtocol] = [:]
|
||||
|
||||
func memberDetailProviderForRoomProxy(_ roomProxy: RoomProxyProtocol) -> MemberDetailProviderProtocol {
|
||||
if let memberDetailProvider = memberDetailProviders[roomProxy.id] {
|
||||
return memberDetailProvider
|
||||
}
|
||||
|
||||
let memberDetailProvider = MemberDetailProvider(roomProxy: roomProxy)
|
||||
memberDetailProviders[roomProxy.id] = memberDetailProvider
|
||||
return memberDetailProvider
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
class MemberDetailProvider: MemberDetailProviderProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private var memberAvatars = [String: String]()
|
||||
private var memberDisplayNames = [String: String]()
|
||||
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
}
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String? {
|
||||
memberAvatars[userId]
|
||||
}
|
||||
|
||||
func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
|
||||
if let avatarURL = avatarURLStringForUserId(userId) {
|
||||
return .success(avatarURL)
|
||||
}
|
||||
|
||||
switch await roomProxy.loadAvatarURLForUserId(userId) {
|
||||
case .success(let avatarURL):
|
||||
memberAvatars[userId] = avatarURL
|
||||
return .success(avatarURL)
|
||||
case .failure:
|
||||
return .failure(.failedRetrievingUserAvatarURL)
|
||||
}
|
||||
}
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String? {
|
||||
memberDisplayNames[userId]
|
||||
}
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
|
||||
if let displayName = displayNameForUserId(userId) {
|
||||
return .success(displayName)
|
||||
}
|
||||
|
||||
switch await roomProxy.loadDisplayNameForUserId(userId) {
|
||||
case .success(let displayName):
|
||||
memberDisplayNames[userId] = displayName
|
||||
return .success(displayName)
|
||||
case .failure:
|
||||
return .failure(.failedRetrievingUserDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
//
|
||||
// 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 MemberDetailProviderError: Error {
|
||||
case invalidRoomProxy
|
||||
case failedRetrievingUserAvatarURL
|
||||
case failedRetrievingUserDisplayName
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol MemberDetailProviderProtocol {
|
||||
func avatarURLStringForUserId(_ userId: String) -> String?
|
||||
func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String?
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct EmoteRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.EmoteMessage
|
||||
|
||||
init(message: MatrixRustSDK.EmoteMessage) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var htmlBody: String? {
|
||||
message.htmlBody()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
//
|
||||
// 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 MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct ImageRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.ImageMessage
|
||||
|
||||
init(message: MatrixRustSDK.ImageMessage) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
}
|
||||
|
||||
var source: MediaSource? {
|
||||
MediaSource(source: message.source())
|
||||
}
|
||||
|
||||
var width: CGFloat? {
|
||||
guard let width = message.width() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CGFloat(width)
|
||||
}
|
||||
|
||||
var height: CGFloat? {
|
||||
guard let height = message.height() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CGFloat(height)
|
||||
}
|
||||
|
||||
var blurhash: String? {
|
||||
message.blurhash()
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct TextRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.TextMessage
|
||||
|
||||
init(message: MatrixRustSDK.TextMessage) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var htmlBody: String? {
|
||||
message.htmlBody()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
}
|
||||
}
|
@ -15,8 +15,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MatrixRustSDK
|
||||
|
||||
struct MockRoomProxy: RoomProxyProtocol {
|
||||
let id = UUID().uuidString
|
||||
@ -24,7 +23,6 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
let displayName: String?
|
||||
|
||||
let topic: String? = nil
|
||||
let messages: [RoomMessageProtocol] = []
|
||||
|
||||
let avatarURL: String? = nil
|
||||
|
||||
@ -34,24 +32,34 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
let isEncrypted = Bool.random()
|
||||
let isTombstoned = Bool.random()
|
||||
|
||||
var callbacks = PassthroughSubject<RoomProxyCallback, Never>()
|
||||
let timelineProvider: RoomTimelineProviderProtocol = MockRoomTimelineProvider()
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
.failure(.failedRetrievingMemberDisplayName)
|
||||
}
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
.failure(.failedRetrievingMemberAvatarURL)
|
||||
}
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadDisplayName() async -> Result<String, RoomProxyError> {
|
||||
.failure(.failedRetrievingDisplayName)
|
||||
}
|
||||
|
||||
func startLiveEventListener() { }
|
||||
|
||||
func addTimelineListener(listener: TimelineListener) { }
|
||||
|
||||
func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError> {
|
||||
.failure(.backwardStreamNotAvailable)
|
||||
.failure(.failedPaginatingBackwards)
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result<Void, RoomProxyError> {
|
||||
|
@ -17,18 +17,15 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct SomeRoomMessage: RoomMessageProtocol {
|
||||
let id: String
|
||||
let body: String
|
||||
let sender: String
|
||||
let originServerTs: Date
|
||||
}
|
||||
|
||||
struct RoomMessageFactory: RoomMessageFactoryProtocol {
|
||||
func buildRoomMessageFrom(_ message: AnyMessage) -> RoomMessageProtocol {
|
||||
if let textMessage = message.textMessage() {
|
||||
return TextRoomMessage(message: textMessage)
|
||||
} else if let imageMessage = message.imageMessage() {
|
||||
return ImageRoomMessage(message: imageMessage)
|
||||
} else if let noticeMessage = message.noticeMessage() {
|
||||
return NoticeRoomMessage(message: noticeMessage)
|
||||
} else if let emoteMessage = message.emoteMessage() {
|
||||
return EmoteRoomMessage(message: emoteMessage)
|
||||
} else {
|
||||
fatalError("One of these must exist")
|
||||
}
|
||||
func buildRoomMessageFrom(_ eventItem: EventTimelineItem) -> RoomMessageProtocol {
|
||||
SomeRoomMessage(id: eventItem.id, body: eventItem.body ?? "", sender: eventItem.sender, originServerTs: eventItem.originServerTs)
|
||||
}
|
||||
}
|
||||
|
@ -18,5 +18,5 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
protocol RoomMessageFactoryProtocol {
|
||||
func buildRoomMessageFrom(_ message: AnyMessage) -> RoomMessageProtocol
|
||||
func buildRoomMessageFrom(_ message: EventTimelineItem) -> RoomMessageProtocol
|
||||
}
|
||||
|
@ -20,55 +20,43 @@ import UIKit
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
private class WeakRoomProxyWrapper: RoomDelegate {
|
||||
private weak var roomProxy: RoomProxy?
|
||||
|
||||
init(roomProxy: RoomProxy) {
|
||||
self.roomProxy = roomProxy
|
||||
}
|
||||
|
||||
// MARK: - RoomDelegate
|
||||
|
||||
func didReceiveMessage(message: AnyMessage) {
|
||||
roomProxy?.appendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class RoomProxy: RoomProxyProtocol {
|
||||
private let room: Room
|
||||
private let roomMessageFactory: RoomMessageFactoryProtocol
|
||||
private let slidingSyncRoom: SlidingSyncRoomProtocol
|
||||
private let room: RoomProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private var backwardStream: BackwardsStreamProtocol?
|
||||
private let concurrentDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy", attributes: .concurrent)
|
||||
|
||||
private var sendMessageBgTask: BackgroundTaskProtocol?
|
||||
|
||||
private var memberAvatars = [String: String]()
|
||||
private var memberDisplayNames = [String: String]()
|
||||
|
||||
private(set) var displayName: String?
|
||||
|
||||
let callbacks = PassthroughSubject<RoomProxyCallback, Never>()
|
||||
|
||||
private(set) var messages: [RoomMessageProtocol]
|
||||
|
||||
init(room: Room,
|
||||
roomMessageFactory: RoomMessageFactoryProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.room = room
|
||||
self.roomMessageFactory = roomMessageFactory
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
messages = []
|
||||
|
||||
room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self))
|
||||
|
||||
backwardStream = room.startLiveEventListener()
|
||||
}
|
||||
private var backPaginationOutcome: PaginationOutcome?
|
||||
private(set) lazy var timelineProvider: RoomTimelineProviderProtocol = {
|
||||
let provider = RoomTimelineProvider(roomProxy: self)
|
||||
addTimelineListener(listener: WeakRoomTimelineProviderWrapper(timelineProvider: provider))
|
||||
return provider
|
||||
}()
|
||||
|
||||
deinit {
|
||||
room.setDelegate(delegate: nil)
|
||||
#warning("Should any timeline listeners be removed??")
|
||||
}
|
||||
|
||||
init(slidingSyncRoom: SlidingSyncRoomProtocol,
|
||||
room: RoomProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.slidingSyncRoom = slidingSyncRoom
|
||||
self.room = room
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
lazy var id: String = room.id()
|
||||
|
||||
var name: String? {
|
||||
room.name()
|
||||
slidingSyncRoom.name()
|
||||
}
|
||||
|
||||
var topic: String? {
|
||||
@ -112,10 +100,15 @@ class RoomProxy: RoomProxyProtocol {
|
||||
return Asset.Images.encryptionTrusted.image
|
||||
}
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String? {
|
||||
memberAvatars[userId]
|
||||
}
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
await Task.detached { () -> Result<String?, RoomProxyError> in
|
||||
do {
|
||||
let avatarURL = try self.room.memberAvatarUrl(userId: userId)
|
||||
await self.update(avatarURL: avatarURL, forUserId: userId)
|
||||
return .success(avatarURL)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingMemberAvatarURL)
|
||||
@ -124,10 +117,15 @@ class RoomProxy: RoomProxyProtocol {
|
||||
.value
|
||||
}
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String? {
|
||||
memberDisplayNames[userId]
|
||||
}
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
await Task.detached { () -> Result<String?, RoomProxyError> in
|
||||
do {
|
||||
let displayName = try self.room.memberDisplayName(userId: userId)
|
||||
await self.update(displayName: displayName, forUserId: userId)
|
||||
return .success(displayName)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingMemberDisplayName)
|
||||
@ -138,14 +136,13 @@ class RoomProxy: RoomProxyProtocol {
|
||||
|
||||
func loadDisplayName() async -> Result<String, RoomProxyError> {
|
||||
await Task.detached { () -> Result<String, RoomProxyError> in
|
||||
if let displayName = self.displayName {
|
||||
if let displayName = await self.displayName {
|
||||
return .success(displayName)
|
||||
}
|
||||
|
||||
do {
|
||||
let displayName = try self.room.displayName()
|
||||
self.displayName = displayName
|
||||
|
||||
await self.update(displayName: displayName)
|
||||
return .success(displayName)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingDisplayName)
|
||||
@ -154,23 +151,27 @@ class RoomProxy: RoomProxyProtocol {
|
||||
.value
|
||||
}
|
||||
|
||||
private func addTimelineListener(listener: TimelineListener) {
|
||||
room.addTimelineListener(listener: listener)
|
||||
}
|
||||
|
||||
func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError> {
|
||||
await Task.detached { () -> Result<Void, RoomProxyError> in
|
||||
guard let backwardStream = self.backwardStream else {
|
||||
return .failure(RoomProxyError.backwardStreamNotAvailable)
|
||||
guard backPaginationOutcome?.moreMessages != false else {
|
||||
return .failure(.noMoreMessagesToBackPaginate)
|
||||
}
|
||||
|
||||
MXLog.debug("BackPagination")
|
||||
return await Task.detached {
|
||||
do {
|
||||
let id = await self.id
|
||||
|
||||
Benchmark.startTrackingForIdentifier("BackPagination \(id)", message: "Backpaginating \(count) message(s) in room \(id)")
|
||||
await self.update(backPaginationOutcome: try self.room.paginateBackwards(limit: UInt16(count)))
|
||||
Benchmark.endTrackingForIdentifier("BackPagination \(id)", message: "Finished backpaginating \(count) message(s) in room \(id)")
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedPaginatingBackwards)
|
||||
}
|
||||
|
||||
Benchmark.startTrackingForIdentifier("BackPagination \(self.id)", message: "Backpaginating \(count) message(s) in room \(self.id)")
|
||||
let sdkMessages = backwardStream.paginateBackwards(count: UInt64(count))
|
||||
Benchmark.endTrackingForIdentifier("BackPagination \(self.id)", message: "Finished backpaginating \(count) message(s) in room \(self.id)")
|
||||
|
||||
let messages = sdkMessages.map { message in
|
||||
self.roomMessageFactory.buildRoomMessageFrom(message)
|
||||
}.reversed()
|
||||
|
||||
self.messages.insert(contentsOf: messages, at: 0)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
.value
|
||||
}
|
||||
@ -180,48 +181,52 @@ class RoomProxy: RoomProxyProtocol {
|
||||
defer {
|
||||
sendMessageBgTask?.stop()
|
||||
}
|
||||
|
||||
|
||||
let transactionId = genTransactionId()
|
||||
|
||||
return await Task(priority: .high) { () -> Result<Void, RoomProxyError> in
|
||||
return await Task.dispatched(on: concurrentDispatchQueue, operation: {
|
||||
do {
|
||||
// Disabled until available in Rust
|
||||
// if let inReplyToEventId = inReplyToEventId {
|
||||
// #warning("Markdown support when available in Ruma")
|
||||
// try self.room.sendReply(msg: message, inReplyToEventId: inReplyToEventId, txnId: transactionId)
|
||||
// } else {
|
||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||
try self.room.send(msg: messageContent, txnId: transactionId)
|
||||
// }
|
||||
if let inReplyToEventId = inReplyToEventId {
|
||||
try self.room.sendReply(msg: message, inReplyToEventId: inReplyToEventId, txnId: transactionId)
|
||||
} else {
|
||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||
try self.room.send(msg: messageContent, txnId: transactionId)
|
||||
}
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedSendingMessage)
|
||||
}
|
||||
})
|
||||
.value
|
||||
}
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||
let transactionID = genTransactionId()
|
||||
|
||||
return await Task {
|
||||
do {
|
||||
try room.redact(eventId: eventID, reason: nil, txnId: transactionID)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedRedactingEvent)
|
||||
}
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||
#warning("Redactions to be enabled on next SDK release.")
|
||||
return .failure(.failedRedactingEvent)
|
||||
// let transactionID = genTransactionId()
|
||||
//
|
||||
// return await Task {
|
||||
// do {
|
||||
// try room.redact(eventId: eventID, reason: nil, txnId: transactionID)
|
||||
// return .success(())
|
||||
// } catch {
|
||||
// return .failure(.failedRedactingEvent)
|
||||
// }
|
||||
// }
|
||||
// .value
|
||||
func update(avatarURL: String?, forUserId userId: String) {
|
||||
memberAvatars[userId] = avatarURL
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
func update(displayName: String?, forUserId userId: String) {
|
||||
memberDisplayNames[userId] = displayName
|
||||
}
|
||||
|
||||
fileprivate func appendMessage(_ message: AnyMessage) {
|
||||
let message = roomMessageFactory.buildRoomMessageFrom(message)
|
||||
messages.append(message)
|
||||
callbacks.send(.updatedMessages)
|
||||
func update(displayName: String) {
|
||||
self.displayName = displayName
|
||||
}
|
||||
|
||||
func update(backPaginationOutcome: PaginationOutcome) {
|
||||
self.backPaginationOutcome = backPaginationOutcome
|
||||
}
|
||||
}
|
||||
|
@ -15,22 +15,20 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomProxyError: Error {
|
||||
case failedRetrievingDisplayName
|
||||
case failedRetrievingAvatar
|
||||
case backwardStreamNotAvailable
|
||||
case noMoreMessagesToBackPaginate
|
||||
case failedPaginatingBackwards
|
||||
case failedRetrievingMemberAvatarURL
|
||||
case failedRetrievingMemberDisplayName
|
||||
case failedSendingMessage
|
||||
case failedRedactingEvent
|
||||
}
|
||||
|
||||
enum RoomProxyCallback {
|
||||
case updatedMessages
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol RoomProxyProtocol {
|
||||
var id: String { get }
|
||||
var isDirect: Bool { get }
|
||||
@ -43,12 +41,17 @@ protocol RoomProxyProtocol {
|
||||
var displayName: String? { get }
|
||||
|
||||
var topic: String? { get }
|
||||
var messages: [RoomMessageProtocol] { get }
|
||||
|
||||
var avatarURL: String? { get }
|
||||
|
||||
var timelineProvider: RoomTimelineProviderProtocol { get }
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String?
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String?
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
|
||||
|
||||
func loadDisplayName() async -> Result<String, RoomProxyError>
|
||||
@ -58,8 +61,6 @@ protocol RoomProxyProtocol {
|
||||
func sendMessage(_ message: String, inReplyToEventId: String?) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
var callbacks: PassthroughSubject<RoomProxyCallback, Never> { get }
|
||||
}
|
||||
|
||||
extension RoomProxyProtocol {
|
||||
|
@ -1,26 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
struct EventBrief {
|
||||
let eventId: String
|
||||
let senderId: String
|
||||
let senderDisplayName: String?
|
||||
let body: String
|
||||
let htmlBody: String?
|
||||
let date: Date
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
struct EventBriefFactory: EventBriefFactoryProtocol {
|
||||
private let memberDetailProvider: MemberDetailProviderProtocol
|
||||
|
||||
init(memberDetailProvider: MemberDetailProviderProtocol) {
|
||||
self.memberDetailProvider = memberDetailProvider
|
||||
}
|
||||
|
||||
func buildEventBriefFor(message: RoomMessageProtocol?) async -> EventBrief? {
|
||||
guard let message = message else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch message {
|
||||
case is ImageRoomMessage:
|
||||
return nil
|
||||
case let message as TextRoomMessage:
|
||||
return await buildEventBrief(message: message, htmlBody: message.htmlBody)
|
||||
case let message as NoticeRoomMessage:
|
||||
return await buildEventBrief(message: message, htmlBody: message.htmlBody)
|
||||
case let message as EmoteRoomMessage:
|
||||
return await buildEventBrief(message: message, htmlBody: message.htmlBody)
|
||||
default:
|
||||
fatalError("Unknown room message.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildEventBrief(message: RoomMessageProtocol, htmlBody: String?) async -> EventBrief? {
|
||||
switch await memberDetailProvider.loadDisplayNameForUserId(message.sender) {
|
||||
case .success(let displayName):
|
||||
return EventBrief(eventId: message.id,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
body: message.body,
|
||||
htmlBody: htmlBody,
|
||||
date: message.originServerTs)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed fetching sender display name with error: \(error)")
|
||||
|
||||
return EventBrief(eventId: message.id,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: nil,
|
||||
body: message.body,
|
||||
htmlBody: htmlBody,
|
||||
date: message.originServerTs)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
//
|
||||
// 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 UIKit
|
||||
|
||||
struct MockRoomSummary: RoomSummaryProtocol {
|
||||
var id: String = UUID().uuidString
|
||||
|
||||
var name: String?
|
||||
|
||||
var displayName: String?
|
||||
|
||||
var topic: String?
|
||||
|
||||
var isDirect = false
|
||||
|
||||
var isEncrypted = false
|
||||
|
||||
var isSpace = false
|
||||
|
||||
var isTombstoned = false
|
||||
|
||||
var lastMessage: EventBrief?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
func loadDetails() async { }
|
||||
|
||||
var callbacks = PassthroughSubject<RoomSummaryCallback, Never>()
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
//
|
||||
// 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 Foundation
|
||||
|
||||
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
var callbacks = PassthroughSubject<RoomSummaryProviderCallback, Never>()
|
||||
var stateUpdatePublisher = CurrentValueSubject<RoomSummaryProviderState, Never>(.cold)
|
||||
var countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
var roomSummaries: [RoomSummary] = [
|
||||
RoomSummary(id: "1", name: "First room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 4),
|
||||
RoomSummary(id: "2",
|
||||
name: "Second room",
|
||||
isDirect: true,
|
||||
avatarURLString: "mockImageURLString",
|
||||
lastMessage: nil,
|
||||
lastMessageTimestamp: nil,
|
||||
unreadNotificationCount: 1),
|
||||
RoomSummary(id: "3",
|
||||
name: "Third room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 0)
|
||||
]
|
||||
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String]) { }
|
||||
}
|
@ -14,147 +14,14 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class RoomSummary: RoomSummaryProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let eventBriefFactory: EventBriefFactoryProtocol
|
||||
|
||||
private var hasLoadedData = false
|
||||
private var roomUpdateListeners = Set<AnyCancellable>()
|
||||
|
||||
var id: String {
|
||||
roomProxy.id
|
||||
}
|
||||
|
||||
var name: String? {
|
||||
roomProxy.name
|
||||
}
|
||||
|
||||
var topic: String? {
|
||||
roomProxy.topic
|
||||
}
|
||||
|
||||
var isDirect: Bool {
|
||||
roomProxy.isDirect
|
||||
}
|
||||
|
||||
var isEncrypted: Bool {
|
||||
roomProxy.isEncrypted
|
||||
}
|
||||
|
||||
var isSpace: Bool {
|
||||
roomProxy.isSpace
|
||||
}
|
||||
|
||||
var isTombstoned: Bool {
|
||||
roomProxy.isTombstoned
|
||||
}
|
||||
|
||||
private(set) var avatar: UIImage? {
|
||||
didSet {
|
||||
callbacks.send(.updatedAvatar)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var displayName: String? {
|
||||
didSet {
|
||||
callbacks.send(.updatedDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var lastMessage: EventBrief? {
|
||||
didSet {
|
||||
callbacks.send(.updatedLastMessage)
|
||||
}
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<RoomSummaryCallback, Never>()
|
||||
|
||||
init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol, eventBriefFactory: EventBriefFactoryProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
self.eventBriefFactory = eventBriefFactory
|
||||
|
||||
Task {
|
||||
lastMessage = await eventBriefFactory.buildEventBriefFor(message: roomProxy.messages.last)
|
||||
}
|
||||
|
||||
roomProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
switch callback {
|
||||
case .updatedMessages:
|
||||
Task {
|
||||
self.lastMessage = await eventBriefFactory.buildEventBriefFor(message: roomProxy.messages.last)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &roomUpdateListeners)
|
||||
}
|
||||
|
||||
func loadDetails() async {
|
||||
if hasLoadedData {
|
||||
return
|
||||
}
|
||||
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
await self.loadDisplayName()
|
||||
}
|
||||
group.addTask {
|
||||
await self.loadAvatar()
|
||||
}
|
||||
group.addTask {
|
||||
await self.loadLastMessage()
|
||||
}
|
||||
}
|
||||
|
||||
hasLoadedData = true
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDisplayName() async {
|
||||
switch await roomProxy.loadDisplayName() {
|
||||
case .success(let displayName):
|
||||
self.displayName = displayName
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed fetching room display name with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAvatar() async {
|
||||
guard let avatarURLString = roomProxy.avatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString) {
|
||||
case .success(let avatar):
|
||||
self.avatar = avatar
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed fetching room avatar with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadLastMessage() async {
|
||||
guard roomProxy.messages.last == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
// Pre-fill the room with some messages and use the last message in the response.
|
||||
switch await roomProxy.paginateBackwards(count: UInt(ClientProxy.syncLimit)) {
|
||||
case .success:
|
||||
lastMessage = await eventBriefFactory.buildEventBriefFor(message: roomProxy.messages.last)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed back paginating with error: \(error)")
|
||||
}
|
||||
}
|
||||
struct RoomSummary {
|
||||
let id: String
|
||||
let name: String
|
||||
let isDirect: Bool
|
||||
let avatarURLString: String?
|
||||
let lastMessage: AttributedString?
|
||||
let lastMessageTimestamp: Date?
|
||||
let unreadNotificationCount: UInt
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
//
|
||||
// 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 UIKit
|
||||
|
||||
enum RoomSummaryCallback {
|
||||
case updatedAvatar
|
||||
case updatedDisplayName
|
||||
case updatedLastMessage
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol RoomSummaryProtocol {
|
||||
var id: String { get }
|
||||
var name: String? { get }
|
||||
var topic: String? { get }
|
||||
var isDirect: Bool { get }
|
||||
var isEncrypted: Bool { get }
|
||||
var isSpace: Bool { get }
|
||||
var isTombstoned: Bool { get }
|
||||
|
||||
var displayName: String? { get }
|
||||
var lastMessage: EventBrief? { get }
|
||||
var avatar: UIImage? { get }
|
||||
|
||||
var callbacks: PassthroughSubject<RoomSummaryCallback, Never> { get }
|
||||
|
||||
func loadDetails() async
|
||||
}
|
@ -0,0 +1,264 @@
|
||||
//
|
||||
// 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 Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
private class WeakRoomSummaryProviderWrapper: SlidingSyncViewRoomListObserver, SlidingSyncViewStateObserver, SlidingSyncViewRoomsCountObserver {
|
||||
private weak var roomSummaryProvider: RoomSummaryProvider?
|
||||
|
||||
/// Publishes room list diffs as they come in through sliding sync
|
||||
let roomListDiffPublisher = PassthroughSubject<SlidingSyncViewRoomsListDiff, Never>()
|
||||
|
||||
/// Publishes the current state of sliding sync, such as whether its catching up or live.
|
||||
let stateUpdatePublisher = CurrentValueSubject<SlidingSyncState, Never>(.cold)
|
||||
|
||||
/// Publishes the number of available rooms
|
||||
let countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
init(roomSummaryProvider: RoomSummaryProvider) {
|
||||
self.roomSummaryProvider = roomSummaryProvider
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncViewRoomListObserver
|
||||
|
||||
func didReceiveUpdate(diff: SlidingSyncViewRoomsListDiff) {
|
||||
roomListDiffPublisher.send(diff)
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncViewStateObserver
|
||||
|
||||
func didReceiveUpdate(newState: SlidingSyncState) {
|
||||
stateUpdatePublisher.send(newState)
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncViewRoomsCountObserver
|
||||
|
||||
func didReceiveUpdate(count: UInt32) {
|
||||
countUpdatePublisher.send(UInt(count))
|
||||
}
|
||||
}
|
||||
|
||||
class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
private let slidingSyncController: SlidingSyncProtocol
|
||||
private let slidingSyncView: SlidingSyncViewProtocol
|
||||
private let roomMessageFactory: RoomMessageFactoryProtocol
|
||||
|
||||
private var listUpdateObserverToken: StoppableSpawn?
|
||||
private var stateUpdateObserverToken: StoppableSpawn?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let callbacks = PassthroughSubject<RoomSummaryProviderCallback, Never>()
|
||||
let stateUpdatePublisher = CurrentValueSubject<RoomSummaryProviderState, Never>(.cold)
|
||||
let countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
private(set) var roomSummaries: [RoomSummary] = [] {
|
||||
didSet {
|
||||
callbacks.send(.updatedRoomSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
listUpdateObserverToken?.cancel()
|
||||
stateUpdateObserverToken?.cancel()
|
||||
}
|
||||
|
||||
init(slidingSyncController: SlidingSyncProtocol, slidingSyncView: SlidingSyncViewProtocol, roomMessageFactory: RoomMessageFactoryProtocol) {
|
||||
self.slidingSyncView = slidingSyncView
|
||||
self.slidingSyncController = slidingSyncController
|
||||
self.roomMessageFactory = roomMessageFactory
|
||||
|
||||
let weakProvider = WeakRoomSummaryProviderWrapper(roomSummaryProvider: self)
|
||||
|
||||
weakProvider.stateUpdatePublisher
|
||||
.map(RoomSummaryProviderState.init)
|
||||
.subscribe(stateUpdatePublisher)
|
||||
.store(in: &cancellables)
|
||||
|
||||
weakProvider.countUpdatePublisher
|
||||
.subscribe(countUpdatePublisher)
|
||||
.store(in: &cancellables)
|
||||
|
||||
weakProvider.roomListDiffPublisher
|
||||
.collect(.byTime(DispatchQueue.global(qos: .background), 0.5))
|
||||
.sink { self.updateRoomsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
listUpdateObserverToken = slidingSyncView.observeRoomList(observer: weakProvider)
|
||||
stateUpdateObserverToken = slidingSyncView.observeState(observer: weakProvider)
|
||||
}
|
||||
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String]) {
|
||||
Task.detached {
|
||||
guard self.stateUpdatePublisher.value == .live else {
|
||||
return
|
||||
}
|
||||
|
||||
var changes = [CollectionDifference<RoomSummary>.Change]()
|
||||
for identifier in identifiers {
|
||||
guard let oldSummary = self.roomSummaries.first(where: { $0.id == identifier }),
|
||||
let index = self.roomSummaries.firstIndex(where: { $0.id == identifier }) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let newSummary = self.buildRoomSummaryForIdentifier(identifier)
|
||||
|
||||
changes.append(.remove(offset: index, element: oldSummary, associatedWith: nil))
|
||||
changes.append(.insert(offset: index, element: newSummary, associatedWith: nil))
|
||||
}
|
||||
|
||||
guard let diff = CollectionDifference(changes) else {
|
||||
MXLog.error("Failed creating diff from changes: \(changes)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let newSummaries = self.roomSummaries.applying(diff) else {
|
||||
MXLog.error("Failed applying diff: \(diff)")
|
||||
return
|
||||
}
|
||||
|
||||
self.roomSummaries = newSummaries
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
fileprivate func updateRoomsWithDiffs(_ diffs: [SlidingSyncViewRoomsListDiff]) {
|
||||
roomSummaries = diffs
|
||||
.compactMap(buildDiff)
|
||||
.reduce(roomSummaries) { $0.applying($1) ?? $0 }
|
||||
}
|
||||
|
||||
private func buildEmptyRoomSummary(forIdentifier identifier: String = UUID().uuidString) -> RoomSummary {
|
||||
RoomSummary(id: identifier,
|
||||
name: "",
|
||||
isDirect: false,
|
||||
avatarURLString: nil,
|
||||
lastMessage: nil,
|
||||
lastMessageTimestamp: nil,
|
||||
unreadNotificationCount: 0)
|
||||
}
|
||||
|
||||
private func buildRoomSummaryForIdentifier(_ identifier: String) -> RoomSummary {
|
||||
guard let room = try? slidingSyncController.getRoom(roomId: identifier) else {
|
||||
MXLog.error("Failed finding room with id: \(identifier)")
|
||||
return buildEmptyRoomSummary(forIdentifier: identifier)
|
||||
}
|
||||
|
||||
var attributedLastMessage: AttributedString?
|
||||
var lastMessageTimestamp: Date?
|
||||
if let latestRoomMessage = room.latestRoomMessage() {
|
||||
let lastMessage = roomMessageFactory.buildRoomMessageFrom(EventTimelineItem(item: latestRoomMessage))
|
||||
if let lastMessageSender = try? AttributedString(markdown: "**\(lastMessage.sender)**") {
|
||||
// Don't include the message body in the markdown otherwise it makes tappable links.
|
||||
attributedLastMessage = lastMessageSender + ": " + AttributedString(lastMessage.body)
|
||||
}
|
||||
lastMessageTimestamp = lastMessage.originServerTs
|
||||
}
|
||||
|
||||
return RoomSummary(id: room.roomId(),
|
||||
name: room.name() ?? room.roomId(),
|
||||
isDirect: room.isDm() ?? false,
|
||||
avatarURLString: room.fullRoom()?.avatarUrl(),
|
||||
lastMessage: attributedLastMessage,
|
||||
lastMessageTimestamp: lastMessageTimestamp,
|
||||
unreadNotificationCount: UInt(room.unreadNotifications().notificationCount()))
|
||||
}
|
||||
|
||||
private func buildSummaryForRoomListEntry(_ entry: RoomListEntry) -> RoomSummary {
|
||||
switch entry {
|
||||
case .empty:
|
||||
return buildEmptyRoomSummary()
|
||||
case .filled(let roomId):
|
||||
return buildRoomSummaryForIdentifier(roomId)
|
||||
case .invalidated(let roomId):
|
||||
return buildRoomSummaryForIdentifier(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
private func buildDiff(from diff: SlidingSyncViewRoomsListDiff) -> CollectionDifference<RoomSummary>? {
|
||||
// Invalidations are a no-op for the moment
|
||||
if diff.isInvalidation {
|
||||
return nil
|
||||
}
|
||||
|
||||
var changes = [CollectionDifference<RoomSummary>.Change]()
|
||||
|
||||
switch diff {
|
||||
case .push(value: let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: Int(roomSummaries.count), element: summary, associatedWith: nil))
|
||||
case .updateAt(let index, let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .insertAt(let index, let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .move(let oldIndex, let newIndex):
|
||||
let summary = roomSummaries[Int(oldIndex)]
|
||||
changes.append(.remove(offset: Int(oldIndex), element: summary, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(newIndex), element: summary, associatedWith: nil))
|
||||
case .removeAt(let index):
|
||||
let summary = roomSummaries[Int(index)]
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .replace(let values):
|
||||
for (index, summary) in roomSummaries.enumerated() {
|
||||
changes.append(.remove(offset: index, element: summary, associatedWith: nil))
|
||||
}
|
||||
|
||||
values
|
||||
.reversed()
|
||||
.map { buildSummaryForRoomListEntry($0) }
|
||||
.forEach { summary in
|
||||
changes.append(.insert(offset: 0, element: summary, associatedWith: nil))
|
||||
}
|
||||
}
|
||||
|
||||
return CollectionDifference(changes)
|
||||
}
|
||||
}
|
||||
|
||||
extension SlidingSyncViewRoomsListDiff {
|
||||
var isInvalidation: Bool {
|
||||
switch self {
|
||||
case .push(let value), .updateAt(_, let value), .insertAt(_, let value):
|
||||
switch value {
|
||||
case .invalidated:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomSummaryProviderState {
|
||||
init(slidingSyncState: SlidingSyncState) {
|
||||
switch slidingSyncState {
|
||||
case .catchingUp:
|
||||
self = .live
|
||||
case .live:
|
||||
self = .live
|
||||
default:
|
||||
self = .cold
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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 Foundation
|
||||
|
||||
enum RoomSummaryProviderCallback {
|
||||
case updatedRoomSummaries
|
||||
}
|
||||
|
||||
enum RoomSummaryProviderState {
|
||||
case cold
|
||||
case live
|
||||
}
|
||||
|
||||
protocol RoomSummaryProviderProtocol {
|
||||
var callbacks: PassthroughSubject<RoomSummaryProviderCallback, Never> { get }
|
||||
|
||||
/// Publishes the current state the summary provider is finding itself in
|
||||
var stateUpdatePublisher: CurrentValueSubject<RoomSummaryProviderState, Never> { get }
|
||||
|
||||
/// Publishes the total number of rooms
|
||||
var countUpdatePublisher: CurrentValueSubject<UInt, Never> { get }
|
||||
|
||||
/// The current list of rooms currently provided by the sliding sync view
|
||||
var roomSummaries: [RoomSummary] { get }
|
||||
|
||||
/// Invoked by the sliding sync controller whenever certain rooms have updated
|
||||
/// without necessarily changing their position in the list
|
||||
/// - Parameter identifiers: the identifiers for the rooms that have changed
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String])
|
||||
}
|
@ -19,7 +19,6 @@ import Combine
|
||||
struct MockUserSession: UserSessionProtocol {
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
|
||||
|
||||
var userID: String { clientProxy.userIdentifier }
|
||||
var isSoftLogout: Bool { clientProxy.isSoftLogout }
|
||||
var deviceId: String? { clientProxy.deviceId }
|
||||
|
@ -76,7 +76,7 @@ class UserSession: UserSessionProtocol {
|
||||
}.store(in: &cancellables)
|
||||
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
|
||||
MXLog.info("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ enum UserSessionCallback {
|
||||
case updateRestoreTokenNeeded
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol UserSessionProtocol {
|
||||
var userID: String { get }
|
||||
var isSoftLogout: Bool { get }
|
||||
|
@ -22,39 +22,74 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
var timelineItems: [RoomTimelineItemProtocol] = [SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Yesterday"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "You rock!",
|
||||
timestamp: "10:10 AM",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Some user with a really long long long long long display name",
|
||||
properties: RoomTimelineItemProperties(isEdited: true)),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "You also rule!",
|
||||
timestamp: "10:11 AM",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Alice",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
|
||||
])),
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Today"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "You too!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: true,
|
||||
senderId: "",
|
||||
senderDisplayName: "Bob",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
||||
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
|
||||
]))]
|
||||
var timelineItems: [RoomTimelineItemProtocol] = [
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Yesterday"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "That looks so good!",
|
||||
timestamp: "10:10 AM",
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Jacob",
|
||||
properties: RoomTimelineItemProperties(isEdited: true)),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗",
|
||||
timestamp: "10:11 AM",
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .beginning,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Helena",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
|
||||
])),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/",
|
||||
timestamp: "10:11 AM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .middle,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Helena",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
||||
AggregatedReaction(key: "🙌", count: 2, isHighlighted: true)
|
||||
])),
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Today"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .end,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Helena",
|
||||
properties: RoomTimelineItemProperties()),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "And John's speech was amazing!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .single,
|
||||
isOutgoing: true,
|
||||
senderId: "",
|
||||
senderDisplayName: "Bob",
|
||||
properties: RoomTimelineItemProperties()),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "New home office set up!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .single,
|
||||
isOutgoing: true,
|
||||
senderId: "",
|
||||
senderDisplayName: "Bob",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
||||
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
|
||||
]))
|
||||
]
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError> {
|
||||
.failure(.generic)
|
||||
|
@ -14,34 +14,21 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
struct NoticeRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.NoticeMessage
|
||||
struct MockRoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
var callbacks = PassthroughSubject<RoomTimelineProviderCallback, Never>()
|
||||
var items = [RoomTimelineProviderItem]()
|
||||
|
||||
init(message: MatrixRustSDK.NoticeMessage) {
|
||||
self.message = message
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError> {
|
||||
.failure(.failedPaginatingBackwards)
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError> {
|
||||
.failure(.failedSendingMessage)
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var htmlBody: String? {
|
||||
message.htmlBody()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
|
||||
.failure(.failedRedactingItem)
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let timelineProvider: RoomTimelineProviderProtocol
|
||||
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let memberDetailProvider: MemberDetailProviderProtocol
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var timelineItemsUpdateTask: Task<Void, Never>? {
|
||||
@ -42,13 +42,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
timelineProvider: RoomTimelineProviderProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
memberDetailProvider: MemberDetailProviderProtocol) {
|
||||
roomProxy: RoomProxyProtocol) {
|
||||
self.userId = userId
|
||||
self.roomId = roomId
|
||||
self.timelineProvider = timelineProvider
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
self.mediaProvider = mediaProvider
|
||||
self.memberDetailProvider = memberDetailProvider
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
self.timelineProvider
|
||||
.callbacks
|
||||
@ -133,29 +133,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
private func asyncUpdateTimelineItems() async {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
|
||||
var previousMessage: RoomMessageProtocol?
|
||||
for message in timelineProvider.messages {
|
||||
|
||||
for (index, item) in timelineProvider.items.enumerated() {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
let previousItem = timelineProvider.items[safe: index - 1]
|
||||
let nextItem = timelineProvider.items[safe: index + 1]
|
||||
|
||||
let inGroupState = inGroupState(for: item, previousItem: previousItem, nextItem: nextItem)
|
||||
|
||||
let areMessagesFromTheSameDay = haveSameDay(lhs: previousMessage, rhs: message)
|
||||
let shouldAddSectionHeader = !areMessagesFromTheSameDay
|
||||
|
||||
if shouldAddSectionHeader {
|
||||
newTimelineItems.append(SeparatorRoomTimelineItem(id: message.originServerTs.ISO8601Format(),
|
||||
text: message.originServerTs.formatted(date: .complete, time: .omitted)))
|
||||
switch item {
|
||||
case .event(let eventItem):
|
||||
guard eventItem.isMessage else { break } // To be handled in the future
|
||||
|
||||
newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(eventItem: eventItem,
|
||||
showSenderDetails: inGroupState.shouldShowSenderDetails,
|
||||
inGroupState: inGroupState))
|
||||
case .virtual:
|
||||
// case .virtual(let virtualItem):
|
||||
// newTimelineItems.append(SeparatorRoomTimelineItem(id: message.originServerTs.ISO8601Format(),
|
||||
// text: message.originServerTs.formatted(date: .complete, time: .omitted)))
|
||||
#warning("Fix the UUID or \"bad things will happen\"")
|
||||
newTimelineItems.append(SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "The day before"))
|
||||
case .other:
|
||||
break
|
||||
}
|
||||
|
||||
let areMessagesFromTheSameSender = (previousMessage?.sender == message.sender)
|
||||
let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay
|
||||
|
||||
newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(message: message,
|
||||
isOutgoing: message.sender == userId,
|
||||
showSenderDetails: shouldShowSenderDetails))
|
||||
|
||||
previousMessage = message
|
||||
}
|
||||
|
||||
if Task.isCancelled {
|
||||
@ -166,13 +171,50 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
callbacks.send(.updatedTimelineItems)
|
||||
}
|
||||
|
||||
private func haveSameDay(lhs: RoomMessageProtocol?, rhs: RoomMessageProtocol?) -> Bool {
|
||||
guard let lhs = lhs, let rhs = rhs else {
|
||||
return false
|
||||
|
||||
private func inGroupState(for item: RoomTimelineProviderItem,
|
||||
previousItem: RoomTimelineProviderItem?,
|
||||
nextItem: RoomTimelineProviderItem?) -> TimelineItemInGroupState {
|
||||
guard let previousItem = previousItem else {
|
||||
// no previous item, check next item
|
||||
guard let nextItem = nextItem else {
|
||||
// no next item neither, this is single
|
||||
return .single
|
||||
}
|
||||
guard nextItem.canBeGrouped(with: item) else {
|
||||
// there is a next item but can't be grouped, this is single
|
||||
return .single
|
||||
}
|
||||
// next will be grouped with this one, this is the start
|
||||
return .beginning
|
||||
}
|
||||
|
||||
return Calendar.current.isDate(lhs.originServerTs, inSameDayAs: rhs.originServerTs)
|
||||
|
||||
guard let nextItem = nextItem else {
|
||||
// no next item
|
||||
guard item.canBeGrouped(with: previousItem) else {
|
||||
// there is a previous item but can't be grouped, this is single
|
||||
return .single
|
||||
}
|
||||
// will be grouped with previous, this is the end
|
||||
return .end
|
||||
}
|
||||
|
||||
guard item.canBeGrouped(with: previousItem) else {
|
||||
guard nextItem.canBeGrouped(with: item) else {
|
||||
// there is a next item but can't be grouped, this is single
|
||||
return .single
|
||||
}
|
||||
// next will be grouped with this one, this is the start
|
||||
return .beginning
|
||||
}
|
||||
|
||||
guard nextItem.canBeGrouped(with: item) else {
|
||||
// there is a next item but can't be grouped, this is the end
|
||||
return .end
|
||||
}
|
||||
|
||||
// next will be grouped with this one, this is the start
|
||||
return .middle
|
||||
}
|
||||
|
||||
private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
|
||||
@ -204,13 +246,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
switch await memberDetailProvider.loadAvatarURLStringForUserId(timelineItem.senderId) {
|
||||
switch await roomProxy.loadAvatarURLForUserId(timelineItem.senderId) {
|
||||
case .success(let avatarURLString):
|
||||
guard let avatarURLString = avatarURLString else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString) {
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString, size: MediaProviderDefaultAvatarSize) {
|
||||
case .success(let avatar):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? EventBasedTimelineItemProtocol else {
|
||||
@ -234,7 +276,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
switch await memberDetailProvider.loadDisplayNameForUserId(timelineItem.senderId) {
|
||||
switch await roomProxy.loadDisplayNameForUserId(timelineItem.senderId) {
|
||||
case .success(let displayName):
|
||||
guard let displayName = displayName,
|
||||
let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
|
@ -15,7 +15,20 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
#warning("Rename to RoomTimelineListener???")
|
||||
class WeakRoomTimelineProviderWrapper: TimelineListener {
|
||||
private weak var timelineProvider: RoomTimelineProvider?
|
||||
|
||||
init(timelineProvider: RoomTimelineProvider) {
|
||||
self.timelineProvider = timelineProvider
|
||||
}
|
||||
|
||||
func onUpdate(update: TimelineDiff) {
|
||||
timelineProvider?.onUpdate(update: update)
|
||||
}
|
||||
}
|
||||
|
||||
class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
@ -23,31 +36,20 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineProviderCallback, Never>()
|
||||
|
||||
private(set) var items: [RoomTimelineProviderItem]
|
||||
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
self.roomProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
case .updatedMessages:
|
||||
self.callbacks.send(.updatedMessages)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
var messages: [RoomMessageProtocol] {
|
||||
roomProxy.messages
|
||||
items = []
|
||||
}
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError> {
|
||||
switch await roomProxy.paginateBackwards(count: count) {
|
||||
case .success:
|
||||
return .success(())
|
||||
case .failure:
|
||||
return .failure(.generic)
|
||||
case .failure(let error):
|
||||
if error == .noMoreMessagesToBackPaginate { return .failure(.noMoreMessagesToBackPaginate) }
|
||||
return .failure(.failedPaginatingBackwards)
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,3 +71,73 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TimelineListener
|
||||
|
||||
private extension RoomTimelineProvider {
|
||||
func onUpdate(update: TimelineDiff) {
|
||||
let change = update.change()
|
||||
MXLog.verbose("Change: \(change)")
|
||||
|
||||
switch change {
|
||||
case .replace:
|
||||
replaceItems(update.replace())
|
||||
case .insertAt:
|
||||
insertItem(update.insertAt())
|
||||
case .updateAt:
|
||||
updateItem(update.updateAt())
|
||||
case .removeAt:
|
||||
removeItem(at: update.removeAt())
|
||||
case .move:
|
||||
moveItem(update.move())
|
||||
case .push:
|
||||
pushItem(update.push())
|
||||
case .pop:
|
||||
popItem()
|
||||
case .clear:
|
||||
clearAllItems()
|
||||
}
|
||||
|
||||
callbacks.send(.updatedMessages)
|
||||
}
|
||||
|
||||
private func replaceItems(_ items: [MatrixRustSDK.TimelineItem]?) {
|
||||
guard let items = items else { return }
|
||||
self.items = items.map(RoomTimelineProviderItem.init)
|
||||
}
|
||||
|
||||
private func insertItem(_ data: InsertAtData?) {
|
||||
guard let data = data else { return }
|
||||
let item = RoomTimelineProviderItem(item: data.item)
|
||||
items.insert(item, at: Int(data.index))
|
||||
}
|
||||
|
||||
private func updateItem(_ data: UpdateAtData?) {
|
||||
guard let data = data else { return }
|
||||
let item = RoomTimelineProviderItem(item: data.item)
|
||||
items[Int(data.index)] = item
|
||||
}
|
||||
|
||||
private func removeItem(at index: UInt32?) {
|
||||
guard let index = index else { return }
|
||||
items.remove(at: Int(index))
|
||||
}
|
||||
|
||||
private func moveItem(_ data: MoveData?) {
|
||||
guard let data = data else { return }
|
||||
items.move(fromOffsets: IndexSet(integer: Int(data.oldIndex)), toOffset: Int(data.newIndex))
|
||||
}
|
||||
|
||||
private func pushItem(_ item: MatrixRustSDK.TimelineItem?) {
|
||||
guard let item = item else { return }
|
||||
items.append(RoomTimelineProviderItem(item: item))
|
||||
}
|
||||
|
||||
private func popItem() {
|
||||
items.removeLast()
|
||||
}
|
||||
|
||||
private func clearAllItems() {
|
||||
items.removeAll()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
//
|
||||
// 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 MatrixRustSDK
|
||||
|
||||
/// A light wrapper around timeline items returned from Rust for use in `RoomTimelineProvider`.
|
||||
enum RoomTimelineProviderItem {
|
||||
case event(EventTimelineItem)
|
||||
case virtual(MatrixRustSDK.VirtualTimelineItem)
|
||||
case other(MatrixRustSDK.TimelineItem)
|
||||
|
||||
init(item: MatrixRustSDK.TimelineItem) {
|
||||
if let eventItem = item.asEvent() {
|
||||
self = .event(EventTimelineItem(item: eventItem))
|
||||
} else if let virtualItem = item.asVirtual() {
|
||||
self = .virtual(virtualItem)
|
||||
} else {
|
||||
self = .other(item)
|
||||
}
|
||||
}
|
||||
|
||||
func canBeGrouped(with prevItem: RoomTimelineProviderItem) -> Bool {
|
||||
guard case let .event(selfEventItem) = self, case let .event(prevEventItem) = prevItem else {
|
||||
return false
|
||||
}
|
||||
// can be improved by adding a date threshold
|
||||
return prevEventItem.reactions.isEmpty && selfEventItem.sender == prevEventItem.sender
|
||||
}
|
||||
}
|
||||
|
||||
/// A light wrapper around event timeline items returned from Rust, used in `RoomTimelineProviderItem`.
|
||||
struct EventTimelineItem {
|
||||
let item: MatrixRustSDK.EventTimelineItem
|
||||
|
||||
init(item: MatrixRustSDK.EventTimelineItem) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
var id: String {
|
||||
#warning("Handle txid in a better way")
|
||||
switch item.key() {
|
||||
case .transactionId(let txnID):
|
||||
return txnID
|
||||
case .eventId(let eventID):
|
||||
return eventID
|
||||
}
|
||||
}
|
||||
|
||||
var body: String? {
|
||||
content.asMessage()?.body()
|
||||
}
|
||||
|
||||
var isMessage: Bool {
|
||||
content.asMessage() != nil
|
||||
}
|
||||
|
||||
var content: TimelineItemContent {
|
||||
item.content()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
item.sender()
|
||||
}
|
||||
|
||||
var reactions: [Reaction] {
|
||||
item.reactions()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
if let timestamp = item.originServerTs() {
|
||||
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
||||
} else {
|
||||
return .now
|
||||
}
|
||||
}
|
||||
}
|
@ -22,16 +22,17 @@ enum RoomTimelineProviderCallback {
|
||||
}
|
||||
|
||||
enum RoomTimelineProviderError: Error {
|
||||
case noMoreMessagesToBackPaginate
|
||||
case failedPaginatingBackwards
|
||||
case failedSendingMessage
|
||||
case failedRedactingItem
|
||||
case generic
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineProviderProtocol {
|
||||
var callbacks: PassthroughSubject<RoomTimelineProviderCallback, Never> { get }
|
||||
|
||||
var messages: [RoomMessageProtocol] { get }
|
||||
var items: [RoomTimelineProviderItem] { get }
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError>
|
||||
|
||||
|
@ -0,0 +1,111 @@
|
||||
//
|
||||
// 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 CoreGraphics
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
/// A protocol that contains the base `m.room.message` event content properties.
|
||||
protocol MessageContentProtocol: RoomMessageEventContentProtocol {
|
||||
var body: String { get }
|
||||
}
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event.
|
||||
struct MessageTimelineItem<Content: MessageContentProtocol> {
|
||||
let item: MatrixRustSDK.EventTimelineItem
|
||||
let content: Content
|
||||
|
||||
var id: String {
|
||||
#warning("Handle txid properly")
|
||||
switch item.key() {
|
||||
case .transactionId(let txnID):
|
||||
return txnID
|
||||
case .eventId(let eventID):
|
||||
return eventID
|
||||
}
|
||||
}
|
||||
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var isEdited: Bool {
|
||||
item.content().asMessage()?.isEdited() == true
|
||||
}
|
||||
|
||||
var inReplyTo: String? {
|
||||
item.content().asMessage()?.inReplyTo()
|
||||
}
|
||||
|
||||
var reactions: [Reaction] {
|
||||
item.reactions()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
item.sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
if let timestamp = item.originServerTs() {
|
||||
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
||||
} else {
|
||||
return .now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Formatted Text
|
||||
|
||||
/// A protocol that contains the expected event content properties for a formatted message.
|
||||
protocol FormattedMessageContentProtocol: MessageContentProtocol {
|
||||
var formatted: FormattedBody? { get }
|
||||
}
|
||||
|
||||
extension MatrixRustSDK.TextMessageContent: FormattedMessageContentProtocol { }
|
||||
extension MatrixRustSDK.EmoteMessageContent: FormattedMessageContentProtocol { }
|
||||
extension MatrixRustSDK.NoticeMessageContent: FormattedMessageContentProtocol { }
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event where
|
||||
/// the `msgtype` would likely contain a formatted body.
|
||||
extension MessageTimelineItem where Content: FormattedMessageContentProtocol {
|
||||
var htmlBody: String? {
|
||||
#warning("Should this check the formatted type?")
|
||||
return content.formatted?.body
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Media
|
||||
|
||||
extension MatrixRustSDK.ImageMessageContent: MessageContentProtocol { }
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
|
||||
var source: MediaSource {
|
||||
MediaSource(source: content.source)
|
||||
}
|
||||
|
||||
var width: CGFloat? {
|
||||
content.info?.width.map(CGFloat.init)
|
||||
}
|
||||
|
||||
var height: CGFloat? {
|
||||
content.info?.height.map(CGFloat.init)
|
||||
}
|
||||
|
||||
var blurhash: String? {
|
||||
content.info?.blurhash
|
||||
}
|
||||
}
|
@ -17,10 +17,40 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum TimelineItemInGroupState {
|
||||
case single
|
||||
case beginning
|
||||
case middle
|
||||
case end
|
||||
|
||||
var roundedCorners: UIRectCorner {
|
||||
switch self {
|
||||
case .single:
|
||||
return .allCorners
|
||||
case .beginning:
|
||||
return [.topLeft, .topRight]
|
||||
case .middle:
|
||||
return []
|
||||
case .end:
|
||||
return [.bottomLeft, .bottomRight]
|
||||
}
|
||||
}
|
||||
|
||||
var shouldShowSenderDetails: Bool {
|
||||
switch self {
|
||||
case .single, .beginning:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
|
||||
var text: String { get }
|
||||
var timestamp: String { get }
|
||||
var shouldShowSenderDetails: Bool { get }
|
||||
var inGroupState: TimelineItemInGroupState { get }
|
||||
var isOutgoing: Bool { get }
|
||||
|
||||
var senderId: String { get }
|
||||
|
@ -23,6 +23,7 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
@ -22,6 +22,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
||||
let text: String
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
@ -23,6 +23,7 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equ
|
||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
@ -23,6 +23,7 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
|
||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
@ -14,46 +14,85 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let memberDetailProvider: MemberDetailProviderProtocol
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol,
|
||||
memberDetailProvider: MemberDetailProviderProtocol,
|
||||
/// The Matrix ID of the current user.
|
||||
private let userID: String
|
||||
|
||||
init(userID: String,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
roomProxy: RoomProxyProtocol,
|
||||
attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
self.userID = userID
|
||||
self.mediaProvider = mediaProvider
|
||||
self.memberDetailProvider = memberDetailProvider
|
||||
self.roomProxy = roomProxy
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
}
|
||||
|
||||
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) async -> RoomTimelineItemProtocol {
|
||||
let displayName = memberDetailProvider.displayNameForUserId(message.sender)
|
||||
let avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender)
|
||||
let avatarImage = mediaProvider.imageFromURLString(avatarURL)
|
||||
func buildTimelineItemFor(eventItem: EventTimelineItem,
|
||||
showSenderDetails: Bool,
|
||||
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol {
|
||||
guard let messageContent = eventItem.content.asMessage() else { fatalError("Must be a message for now.") }
|
||||
let displayName = roomProxy.displayNameForUserId(eventItem.sender)
|
||||
let avatarURL = roomProxy.avatarURLStringForUserId(eventItem.sender)
|
||||
let avatarImage = mediaProvider.imageFromURLString(avatarURL, size: MediaProviderDefaultAvatarSize)
|
||||
let isOutgoing = eventItem.sender == userID
|
||||
|
||||
switch message {
|
||||
case let message as TextRoomMessage:
|
||||
return await buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
case let message as ImageRoomMessage:
|
||||
return await buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
case let message as NoticeRoomMessage:
|
||||
return await buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
case let message as EmoteRoomMessage:
|
||||
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
default:
|
||||
fatalError("Unknown room message.")
|
||||
switch messageContent.msgtype() {
|
||||
case .text(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .image(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .notice(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .emote(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .none:
|
||||
return await buildFallbackTimelineItem(eventItem, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildTextTimelineItemFromMessage(_ message: TextRoomMessage,
|
||||
// swiftformat:disable function_parameter_count
|
||||
// swiftlint:disable function_parameter_count
|
||||
private func buildFallbackTimelineItem(_ item: EventTimelineItem,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await attributedStringBuilder.fromPlain(item.body)
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return TextRoomTimelineItem(id: item.id,
|
||||
text: item.body ?? "",
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: item.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: item.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: item.content.asMessage()?.isEdited() ?? false,
|
||||
reactions: aggregateReactions(item.reactions)))
|
||||
}
|
||||
|
||||
private func buildTextTimelineItemFromMessage(_ message: MessageTimelineItem<TextMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@ -64,15 +103,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage)
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildImageTimelineItemFromMessage(_ message: ImageRoomMessage,
|
||||
private func buildImageTimelineItemFromMessage(_ message: MessageTimelineItem<ImageMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@ -85,6 +128,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
text: message.body,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
@ -94,12 +138,15 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
width: message.width,
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
blurhash: message.blurhash)
|
||||
blurhash: message.blurhash,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildNoticeTimelineItemFromMessage(_ message: NoticeRoomMessage,
|
||||
private func buildNoticeTimelineItemFromMessage(_ message: MessageTimelineItem<NoticeMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@ -110,15 +157,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage)
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildEmoteTimelineItemFromMessage(_ message: EmoteRoomMessage,
|
||||
private func buildEmoteTimelineItemFromMessage(_ message: MessageTimelineItem<EmoteMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@ -129,9 +180,22 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage)
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
// swiftlint:enable function_parameter_count
|
||||
// swiftformat:enable function_parameter_count
|
||||
|
||||
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
|
||||
reactions.map { reaction in
|
||||
let isHighlighted = false // reaction.details.contains(where: { $0.sender == userID })
|
||||
return AggregatedReaction(key: reaction.key, count: Int(reaction.count), isHighlighted: isHighlighted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,5 +18,7 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineItemFactoryProtocol {
|
||||
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) async -> RoomTimelineItemProtocol
|
||||
func buildTimelineItemFor(eventItem: EventTimelineItem,
|
||||
showSenderDetails: Bool,
|
||||
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol
|
||||
}
|
||||
|
@ -110,14 +110,12 @@ class MockScreen: Identifiable {
|
||||
case .roomPlainNoAvatar:
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(),
|
||||
roomName: "Some room name",
|
||||
roomAvatar: nil,
|
||||
roomEncryptionBadge: nil)
|
||||
roomAvatar: nil)
|
||||
return RoomScreenCoordinator(parameters: parameters)
|
||||
case .roomEncryptedWithAvatar:
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(),
|
||||
roomName: "Some room name",
|
||||
roomAvatar: Asset.Images.appLogo.image,
|
||||
roomEncryptionBadge: Asset.Images.encryptionTrusted.image)
|
||||
roomAvatar: Asset.Images.appLogo.image)
|
||||
return RoomScreenCoordinator(parameters: parameters)
|
||||
case .sessionVerification:
|
||||
let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: MockSessionVerificationControllerProxy())
|
||||
|
@ -22,10 +22,8 @@ class SessionVerificationUITests: XCTestCase {
|
||||
let app = Application.launch()
|
||||
app.goToScreenWithIdentifier(.sessionVerification)
|
||||
|
||||
XCTAssert(app.navigationBars[ElementL10n.verificationVerifyDevice].exists)
|
||||
|
||||
XCTAssert(app.buttons["startButton"].exists)
|
||||
XCTAssert(app.buttons["dismissButton"].exists)
|
||||
XCTAssert(app.buttons["closeButton"].exists)
|
||||
XCTAssert(app.staticTexts["titleLabel"].exists)
|
||||
|
||||
app.assertScreenshot(.sessionVerification)
|
||||
@ -33,72 +31,63 @@ class SessionVerificationUITests: XCTestCase {
|
||||
app.buttons["startButton"].tap()
|
||||
|
||||
XCTAssert(app.activityIndicators["requestingVerificationProgressView"].exists)
|
||||
XCTAssert(app.buttons["cancelButton"].exists)
|
||||
|
||||
XCTAssert(app.buttons["challengeAcceptButton"].waitForExistence(timeout: 5.0))
|
||||
XCTAssert(app.buttons["challengeDeclineButton"].waitForExistence(timeout: 5.0))
|
||||
XCTAssert(app.buttons["cancelButton"].waitForExistence(timeout: 5.0))
|
||||
|
||||
app.buttons["challengeAcceptButton"].tap()
|
||||
|
||||
XCTAssert(app.activityIndicators["acceptingChallengeProgressView"].exists)
|
||||
XCTAssert(app.buttons["cancelButton"].exists)
|
||||
|
||||
XCTAssert(app.images["sessionVerificationSucceededIcon"].waitForExistence(timeout: 5.0))
|
||||
|
||||
XCTAssert(app.buttons["dismissButton"].exists)
|
||||
app.buttons["dismissButton"].tap()
|
||||
XCTAssert(app.buttons["finishButton"].exists)
|
||||
XCTAssert(app.buttons["closeButton"].exists)
|
||||
app.buttons["closeButton"].tap()
|
||||
}
|
||||
|
||||
func testChallengeDoesNotMatch() {
|
||||
let app = Application.launch()
|
||||
app.goToScreenWithIdentifier(.sessionVerification)
|
||||
|
||||
XCTAssert(app.navigationBars[ElementL10n.verificationVerifyDevice].exists)
|
||||
|
||||
XCTAssert(app.buttons["startButton"].exists)
|
||||
XCTAssert(app.buttons["dismissButton"].exists)
|
||||
XCTAssert(app.buttons["closeButton"].exists)
|
||||
XCTAssert(app.staticTexts["titleLabel"].exists)
|
||||
|
||||
app.buttons["startButton"].tap()
|
||||
|
||||
XCTAssert(app.activityIndicators["requestingVerificationProgressView"].exists)
|
||||
XCTAssert(app.buttons["cancelButton"].exists)
|
||||
|
||||
XCTAssert(app.buttons["challengeAcceptButton"].waitForExistence(timeout: 5.0))
|
||||
XCTAssert(app.buttons["challengeDeclineButton"].waitForExistence(timeout: 5.0))
|
||||
XCTAssert(app.buttons["cancelButton"].waitForExistence(timeout: 5.0))
|
||||
|
||||
app.buttons["challengeDeclineButton"].tap()
|
||||
|
||||
XCTAssert(app.images["sessionVerificationFailedIcon"].exists)
|
||||
XCTAssert(app.buttons["restartButton"].exists)
|
||||
|
||||
XCTAssert(app.buttons["dismissButton"].exists)
|
||||
app.buttons["dismissButton"].tap()
|
||||
XCTAssert(app.buttons["closeButton"].exists)
|
||||
app.buttons["closeButton"].tap()
|
||||
}
|
||||
|
||||
func testSessionVerificationCancelation() {
|
||||
let app = Application.launch()
|
||||
app.goToScreenWithIdentifier(.sessionVerification)
|
||||
|
||||
XCTAssert(app.navigationBars[ElementL10n.verificationVerifyDevice].exists)
|
||||
|
||||
XCTAssert(app.buttons["startButton"].exists)
|
||||
XCTAssert(app.buttons["dismissButton"].exists)
|
||||
XCTAssert(app.buttons["closeButton"].exists)
|
||||
XCTAssert(app.staticTexts["titleLabel"].exists)
|
||||
|
||||
app.buttons["startButton"].tap()
|
||||
|
||||
XCTAssert(app.activityIndicators["requestingVerificationProgressView"].waitForExistence(timeout: 1))
|
||||
XCTAssert(app.buttons["cancelButton"].exists)
|
||||
|
||||
app.buttons["cancelButton"].tap()
|
||||
app.buttons["closeButton"].tap()
|
||||
|
||||
XCTAssert(app.images["sessionVerificationFailedIcon"].waitForExistence(timeout: 1))
|
||||
XCTAssert(app.buttons["restartButton"].exists)
|
||||
|
||||
XCTAssert(app.buttons["dismissButton"].exists)
|
||||
app.buttons["dismissButton"].tap()
|
||||
XCTAssert(app.buttons["closeButton"].exists)
|
||||
app.buttons["closeButton"].tap()
|
||||
}
|
||||
}
|
||||
|
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.roomEncryptedWithAvatar.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.roomEncryptedWithAvatar.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.roomPlainNoAvatar.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.roomPlainNoAvatar.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.sessionVerification.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.sessionVerification.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.settings.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPad-9th-generation.settings.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPhone-13-Pro-Max.roomEncryptedWithAvatar.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPhone-13-Pro-Max.roomEncryptedWithAvatar.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPhone-13-Pro-Max.roomPlainNoAvatar.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/15-5-de-DE-iPhone-13-Pro-Max.roomPlainNoAvatar.png
(Stored with Git LFS)
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user