mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Render state items in the timeline and as the last message on the home screen. (#473)
* State events in the timeline WIP * Implement membership change strings. * Add other state event types and use in RoomSummaryProvider. Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -58,6 +58,9 @@
|
||||
1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; };
|
||||
172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; };
|
||||
187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */; };
|
||||
18E674DB2977DBD60055EA9F /* StateRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */; };
|
||||
18E674DD2977DC2B0055EA9F /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */; };
|
||||
18E674DF2977DD9B0055EA9F /* RoomStateStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DE2977DD9B0055EA9F /* RoomStateStringBuilder.swift */; };
|
||||
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 */; };
|
||||
@@ -283,6 +286,7 @@
|
||||
8F2FAA98457750D9D664136F /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; };
|
||||
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; };
|
||||
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
|
||||
910D302D29795F110093B842 /* RoomEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910D302C29795F110093B842 /* RoomEventStringBuilder.swift */; };
|
||||
91DFCB641FBA03EE2DA0189E /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */; };
|
||||
9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; };
|
||||
92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */; };
|
||||
@@ -580,6 +584,9 @@
|
||||
1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = "<group>"; };
|
||||
179423E34EE846E048E49CBF /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = "<group>"; };
|
||||
184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
18E674DE2977DD9B0055EA9F /* RoomStateStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateStringBuilder.swift; sourceTree = "<group>"; };
|
||||
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = "<group>"; };
|
||||
18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = "<group>"; };
|
||||
1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
@@ -679,7 +686,7 @@
|
||||
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
|
||||
471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; };
|
||||
478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; };
|
||||
4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
|
||||
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||
48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -806,7 +813,7 @@
|
||||
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>"; };
|
||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||
8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = "<group>"; };
|
||||
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||
8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = "<group>"; };
|
||||
@@ -814,6 +821,7 @@
|
||||
9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = "<group>"; };
|
||||
9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserNotificationController.swift; sourceTree = "<group>"; };
|
||||
90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
910D302C29795F110093B842 /* RoomEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomEventStringBuilder.swift; sourceTree = "<group>"; };
|
||||
91FB6F5ECCF51ECE98ACFEEC /* RoomDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModel.swift; sourceTree = "<group>"; };
|
||||
9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreen.swift; sourceTree = "<group>"; };
|
||||
92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = "<group>"; };
|
||||
@@ -1019,7 +1027,7 @@
|
||||
EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; 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>"; };
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; 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>"; };
|
||||
@@ -1648,6 +1656,7 @@
|
||||
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */,
|
||||
142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */,
|
||||
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */,
|
||||
910D302C29795F110093B842 /* RoomEventStringBuilder.swift */,
|
||||
10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */,
|
||||
);
|
||||
path = RoomSummary;
|
||||
@@ -1983,6 +1992,7 @@
|
||||
EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */,
|
||||
ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */,
|
||||
75D1D02F7F3AC1122FCFB4F3 /* Items */,
|
||||
18E674DE2977DD9B0055EA9F /* RoomStateStringBuilder.swift */,
|
||||
);
|
||||
path = TimelineItems;
|
||||
sourceTree = "<group>";
|
||||
@@ -2116,6 +2126,7 @@
|
||||
E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */,
|
||||
818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */,
|
||||
F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */,
|
||||
18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */,
|
||||
);
|
||||
path = Other;
|
||||
sourceTree = "<group>";
|
||||
@@ -2163,6 +2174,7 @@
|
||||
F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */,
|
||||
A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */,
|
||||
1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */,
|
||||
18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */,
|
||||
);
|
||||
path = Timeline;
|
||||
sourceTree = "<group>";
|
||||
@@ -2965,6 +2977,7 @@
|
||||
64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */,
|
||||
D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */,
|
||||
E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */,
|
||||
18E674DF2977DD9B0055EA9F /* RoomStateStringBuilder.swift in Sources */,
|
||||
6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */,
|
||||
38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */,
|
||||
B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */,
|
||||
@@ -2995,6 +3008,7 @@
|
||||
3910D3A2EF98587C0E7B9CCB /* EmojiMartEmoji.swift in Sources */,
|
||||
7E3C34BC10936AD4F77975F4 /* EmojiMartJSONLoader.swift in Sources */,
|
||||
92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */,
|
||||
18E674DB2977DBD60055EA9F /* StateRoomTimelineItem.swift in Sources */,
|
||||
1A8BDEB96C3B2F033FA563F8 /* EmojiPickerHeaderView.swift in Sources */,
|
||||
340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */,
|
||||
C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */,
|
||||
@@ -3039,6 +3053,7 @@
|
||||
CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */,
|
||||
38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */,
|
||||
5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */,
|
||||
18E674DD2977DC2B0055EA9F /* StateRoomTimelineView.swift in Sources */,
|
||||
BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */,
|
||||
49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */,
|
||||
2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */,
|
||||
@@ -3179,6 +3194,7 @@
|
||||
44AE0752E001D1D10605CD88 /* Swipe.swift in Sources */,
|
||||
E290C78E7F09F47FD2662986 /* Task.swift in Sources */,
|
||||
43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */,
|
||||
910D302D29795F110093B842 /* RoomEventStringBuilder.swift in Sources */,
|
||||
63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */,
|
||||
1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */,
|
||||
75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
version = "1.7">
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -39,20 +38,6 @@
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "32C23C8D224D46EFE62AFAD0"
|
||||
BuildableName = "UnitTests.xctest"
|
||||
BlueprintName = "UnitTests"
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "IS_RUNNING_UNIT_TESTS"
|
||||
@@ -69,6 +54,18 @@
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "32C23C8D224D46EFE62AFAD0"
|
||||
BuildableName = "UnitTests.xctest"
|
||||
BlueprintName = "UnitTests"
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -90,8 +87,6 @@
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
@@ -101,7 +96,7 @@
|
||||
<EnvironmentVariable
|
||||
key = "HTTPS_PROXY"
|
||||
value = "192.168.0.111:9090"
|
||||
isEnabled = "NO">
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
@@ -121,8 +116,6 @@
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -24,6 +24,20 @@
|
||||
|
||||
"room_timeline_item_unsupported" = "Unsupported event";
|
||||
|
||||
"noticeRoomInviteAccepted" = "%1$@ accepted the invite";
|
||||
"noticeRoomInviteAcceptedByYou" = "You accepted the invite";
|
||||
"noticeRoomKnock" = "%1$@ requested to join";
|
||||
"noticeRoomKnockByYou" = "You requested to join";
|
||||
"noticeRoomKnockAccepted" = "%1$@ allowed %2$@ to join";
|
||||
"noticeRoomKnockAcceptedByYou" = "%1$@ allowed you to join";
|
||||
"noticeRoomKnockRetracted" = "%1$@ is no longer interested in joining";
|
||||
"noticeRoomKnockRetractedByYou" = "You cancelled your request to join";
|
||||
"noticeRoomKnockDenied" = "%1$@ rejected %2$@'s request to join";
|
||||
"noticeRoomKnockDeniedByYou" = "You rejected %1$@'s request to join";
|
||||
"noticeRoomKnockDeniedYou" = "%1$@ rejected your request to join";
|
||||
"noticeRoomUnknownMembershipChange" = "%1$@ made an unknown change to their membership";
|
||||
|
||||
|
||||
"session_verification_banner_title" = "Help keep your messages secure";
|
||||
"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you.";
|
||||
|
||||
|
||||
@@ -24,6 +24,48 @@ extension ElementL10n {
|
||||
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
||||
/// Tablet
|
||||
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
|
||||
/// %1$@ accepted the invite
|
||||
public static func noticeRoomInviteAccepted(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomInviteAccepted", String(describing: p1))
|
||||
}
|
||||
/// You accepted the invite
|
||||
public static let noticeRoomInviteAcceptedByYou = ElementL10n.tr("Untranslated", "noticeRoomInviteAcceptedByYou")
|
||||
/// %1$@ requested to join
|
||||
public static func noticeRoomKnock(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomKnock", String(describing: p1))
|
||||
}
|
||||
/// %1$@ allowed %2$@ to join
|
||||
public static func noticeRoomKnockAccepted(_ p1: Any, _ p2: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomKnockAccepted", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// %1$@ allowed you to join
|
||||
public static func noticeRoomKnockAcceptedByYou(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomKnockAcceptedByYou", String(describing: p1))
|
||||
}
|
||||
/// You requested to join
|
||||
public static let noticeRoomKnockByYou = ElementL10n.tr("Untranslated", "noticeRoomKnockByYou")
|
||||
/// %1$@ rejected %2$@'s request to join
|
||||
public static func noticeRoomKnockDenied(_ p1: Any, _ p2: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomKnockDenied", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// You rejected %1$@'s request to join
|
||||
public static func noticeRoomKnockDeniedByYou(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomKnockDeniedByYou", String(describing: p1))
|
||||
}
|
||||
/// %1$@ rejected your request to join
|
||||
public static func noticeRoomKnockDeniedYou(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomKnockDeniedYou", String(describing: p1))
|
||||
}
|
||||
/// %1$@ is no longer interested in joining
|
||||
public static func noticeRoomKnockRetracted(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomKnockRetracted", String(describing: p1))
|
||||
}
|
||||
/// You cancelled your request to join
|
||||
public static let noticeRoomKnockRetractedByYou = ElementL10n.tr("Untranslated", "noticeRoomKnockRetractedByYou")
|
||||
/// %1$@ made an unknown change to their membership
|
||||
public static func noticeRoomUnknownMembershipChange(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomUnknownMembershipChange", String(describing: p1))
|
||||
}
|
||||
/// Notification
|
||||
public static let notification = ElementL10n.tr("Untranslated", "Notification")
|
||||
/// About
|
||||
|
||||
@@ -213,7 +213,7 @@ struct HomeScreen_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
static func body(_ state: MockRoomSummaryProviderState) -> some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe",
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe",
|
||||
roomSummaryProvider: MockRoomSummaryProvider(state: state)),
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ struct HomeScreenRoomCell_Previews: PreviewProvider {
|
||||
static var body: some View {
|
||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded)
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe", roomSummaryProvider: summaryProvider),
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: summaryProvider),
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Copyright 2023 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 StateRoomTimelineView: View {
|
||||
let timelineItem: StateRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
Text(timelineItem.text)
|
||||
.font(.element.caption1Bold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
struct StateRoomTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
let item = StateRoomTimelineItem(id: UUID().uuidString, text: "Alice joined", timestamp: "Now", groupState: .beginning, isOutgoing: false, isEditable: false, sender: .init(id: ""))
|
||||
return StateRoomTimelineView(timelineItem: item)
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@ extension TimelineStyle: CustomStringConvertible {
|
||||
|
||||
struct Settings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@userid:example.com"),
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
let viewModel = SettingsViewModel(withUserSession: userSession)
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
return .failure(.invalidCredentials)
|
||||
}
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: username),
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: username),
|
||||
mediaProvider: MockMediaProvider())
|
||||
return .success(userSession)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
configureSlidingSync()
|
||||
}
|
||||
|
||||
var userIdentifier: String {
|
||||
var userID: String {
|
||||
do {
|
||||
return try client.userId()
|
||||
} catch {
|
||||
@@ -291,7 +291,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
private func buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: SlidingSyncProtocol, visibleRoomsView: SlidingSyncView) {
|
||||
let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView)
|
||||
|
||||
visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy)
|
||||
visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy,
|
||||
eventStringBuilder: RoomEventStringBuilder(userID: userID,
|
||||
roomStateStringBuilder: RoomStateStringBuilder(userID: userID)))
|
||||
|
||||
visibleRoomsSlidingSyncView = visibleRoomsView
|
||||
|
||||
@@ -327,7 +329,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
let allRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: allRoomsView)
|
||||
|
||||
allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy)
|
||||
allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy,
|
||||
eventStringBuilder: RoomEventStringBuilder(userID: userID,
|
||||
roomStateStringBuilder: RoomStateStringBuilder(userID: userID)))
|
||||
|
||||
allRoomsSlidingSyncView = allRoomsView
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ enum PushFormat {
|
||||
protocol ClientProxyProtocol: AnyObject, MediaProxyProtocol {
|
||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||
|
||||
var userIdentifier: String { get }
|
||||
var userID: String { get }
|
||||
|
||||
var isSoftLogout: Bool { get }
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import MatrixRustSDK
|
||||
class MockClientProxy: ClientProxyProtocol {
|
||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||
|
||||
let userIdentifier: String
|
||||
let userID: String
|
||||
let isSoftLogout = false
|
||||
let deviceId: String? = nil
|
||||
let homeserver = ""
|
||||
@@ -31,8 +31,8 @@ class MockClientProxy: ClientProxyProtocol {
|
||||
|
||||
var allRoomsSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
||||
|
||||
internal init(userIdentifier: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) {
|
||||
self.userIdentifier = userIdentifier
|
||||
internal init(userID: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) {
|
||||
self.userID = userID
|
||||
visibleRoomsSummaryProvider = roomSummaryProvider
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Copyright 2023 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 RoomEventStringBuilder {
|
||||
private let roomStateStringBuilder: RoomStateStringBuilder
|
||||
|
||||
/// The Matrix ID of the current user.
|
||||
private let userID: String
|
||||
|
||||
init(userID: String, roomStateStringBuilder: RoomStateStringBuilder) {
|
||||
self.userID = userID
|
||||
self.roomStateStringBuilder = roomStateStringBuilder
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? {
|
||||
let sender = eventItemProxy.sender
|
||||
let isOutgoing = eventItemProxy.isOwn
|
||||
|
||||
switch eventItemProxy.content.kind() {
|
||||
case .unableToDecrypt:
|
||||
return prefix(ElementL10n.encryptionInformationDecryptionError, with: sender)
|
||||
case .redactedMessage:
|
||||
return prefix(ElementL10n.eventRedacted, with: sender)
|
||||
case .sticker:
|
||||
return prefix(ElementL10n.sendASticker, with: sender)
|
||||
case .failedToParseMessageLike, .failedToParseState:
|
||||
return prefix(ElementL10n.roomTimelineItemUnsupported, with: sender)
|
||||
case .message:
|
||||
guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") }
|
||||
|
||||
let message: String
|
||||
switch messageContent.msgtype() {
|
||||
// Message types that don't need a prefix.
|
||||
case .emote(content: let content):
|
||||
let senderDisplayName = sender.displayName ?? sender.id
|
||||
return AttributedString("* \(senderDisplayName) \(content.body)")
|
||||
// Message types that should be prefixed with the sender's name.
|
||||
case .image:
|
||||
message = ElementL10n.sentAnImage
|
||||
case .video:
|
||||
message = ElementL10n.sentAVideo
|
||||
case .file:
|
||||
message = ElementL10n.sentAFile
|
||||
default:
|
||||
message = messageContent.body()
|
||||
}
|
||||
return prefix(message, with: sender)
|
||||
case .state(let stateKey, let state):
|
||||
return roomStateStringBuilder
|
||||
.buildString(for: state, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing)
|
||||
.map(AttributedString.init)
|
||||
case .roomMembership(userId: let userID, change: let change):
|
||||
return roomStateStringBuilder
|
||||
.buildString(for: change, member: userID, sender: sender, isOutgoing: isOutgoing)
|
||||
.map(AttributedString.init)
|
||||
}
|
||||
}
|
||||
|
||||
func prefix(_ eventSummary: String, with sender: TimelineItemSender) -> AttributedString {
|
||||
if let senderDisplayName = sender.displayName,
|
||||
let attributedSenderDisplayName = try? AttributedString(markdown: "**\(senderDisplayName)**") {
|
||||
// Don't include the message body in the markdown otherwise it makes tappable links.
|
||||
return attributedSenderDisplayName + ": " + AttributedString(eventSummary)
|
||||
} else {
|
||||
return AttributedString(eventSummary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import MatrixRustSDK
|
||||
class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
private let slidingSyncViewProxy: SlidingSyncViewProxy
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
private let eventStringBuilder: RoomEventStringBuilder
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@@ -34,9 +35,10 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
init(slidingSyncViewProxy: SlidingSyncViewProxy) {
|
||||
init(slidingSyncViewProxy: SlidingSyncViewProxy, eventStringBuilder: RoomEventStringBuilder) {
|
||||
self.slidingSyncViewProxy = slidingSyncViewProxy
|
||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider")
|
||||
self.eventStringBuilder = eventStringBuilder
|
||||
|
||||
rooms = slidingSyncViewProxy.currentRoomsList().map { roomListEntry in
|
||||
buildSummaryForRoomListEntry(roomListEntry)
|
||||
@@ -151,16 +153,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
DispatchQueue.global(qos: .default).sync {
|
||||
if let latestRoomMessage = room.latestRoomMessage() {
|
||||
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage)
|
||||
|
||||
lastMessageTimestamp = lastMessage.timestamp
|
||||
|
||||
if let senderDisplayName = lastMessage.sender.displayName,
|
||||
let attributedSenderDisplayName = try? AttributedString(markdown: "**\(senderDisplayName)**") {
|
||||
// Don't include the message body in the markdown otherwise it makes tappable links.
|
||||
attributedLastMessage = attributedSenderDisplayName + ": " + AttributedString(lastMessage.body ?? "")
|
||||
} else if let body = lastMessage.body {
|
||||
attributedLastMessage = AttributedString(body)
|
||||
}
|
||||
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import Combine
|
||||
struct MockUserSession: UserSessionProtocol {
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
|
||||
var userID: String { clientProxy.userIdentifier }
|
||||
var userID: String { clientProxy.userID }
|
||||
var isSoftLogout: Bool { clientProxy.isSoftLogout }
|
||||
var deviceId: String? { clientProxy.deviceId }
|
||||
var homeserver: String { clientProxy.homeserver }
|
||||
|
||||
@@ -23,7 +23,7 @@ class UserSession: UserSessionProtocol {
|
||||
private var authErrorCancellable: AnyCancellable?
|
||||
private var restoreTokenUpdateCancellable: AnyCancellable?
|
||||
|
||||
var userID: String { clientProxy.userIdentifier }
|
||||
var userID: String { clientProxy.userID }
|
||||
var isSoftLogout: Bool { clientProxy.isSoftLogout }
|
||||
var deviceId: String? { clientProxy.deviceId }
|
||||
var homeserver: String { clientProxy.homeserver }
|
||||
|
||||
@@ -259,9 +259,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
let groupState = computeGroupState(for: itemProxy, previousItemProxy: previousItemProxy, nextItemProxy: nextItemProxy)
|
||||
|
||||
switch itemProxy {
|
||||
case .event(let eventItem):
|
||||
newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItem,
|
||||
groupState: groupState))
|
||||
case .event(let eventItemProxy):
|
||||
if let timelineItem = timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItemProxy, groupState: groupState) {
|
||||
newTimelineItems.append(timelineItem)
|
||||
}
|
||||
case .virtual(let virtualItem):
|
||||
switch virtualItem {
|
||||
case .dayDivider(let year, let month, let day):
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Copyright 2023 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 UIKit
|
||||
|
||||
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||
let id: String
|
||||
let text: String
|
||||
let timestamp: String
|
||||
let groupState: TimelineItemGroupState
|
||||
let isOutgoing: Bool
|
||||
let isEditable: Bool
|
||||
|
||||
var sender: TimelineItemSender
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
//
|
||||
// Copyright 2023 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 RoomStateStringBuilder {
|
||||
let userID: String
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
func buildString(for change: MembershipChange, member: String, sender: TimelineItemSender, isOutgoing: Bool) -> String? {
|
||||
let senderName = sender.displayName ?? sender.id
|
||||
let senderIsYou = isOutgoing
|
||||
let memberIsYou = member == userID
|
||||
|
||||
switch change {
|
||||
case .joined:
|
||||
return memberIsYou ? ElementL10n.noticeRoomJoinByYou : ElementL10n.noticeRoomJoin(member)
|
||||
case .left:
|
||||
return memberIsYou ? ElementL10n.noticeRoomLeaveByYou : ElementL10n.noticeRoomLeave(member)
|
||||
case .banned, .kickedAndBanned:
|
||||
return senderIsYou ? ElementL10n.noticeRoomBanByYou(member) : ElementL10n.noticeRoomBan(senderName, member)
|
||||
case .unbanned:
|
||||
return senderIsYou ? ElementL10n.noticeRoomUnbanByYou(member) : ElementL10n.noticeRoomUnban(senderName, member)
|
||||
case .kicked:
|
||||
return senderIsYou ? ElementL10n.noticeRoomRemoveByYou(member) : ElementL10n.noticeRoomRemove(senderName, member)
|
||||
case .invited:
|
||||
if senderIsYou {
|
||||
return ElementL10n.noticeRoomInviteByYou(member)
|
||||
} else if memberIsYou {
|
||||
return ElementL10n.noticeRoomInviteYou(senderName)
|
||||
} else {
|
||||
return ElementL10n.noticeRoomInvite(senderName, member)
|
||||
}
|
||||
case .invitationAccepted:
|
||||
return memberIsYou ? ElementL10n.noticeRoomInviteAcceptedByYou : ElementL10n.noticeRoomInviteAccepted(member)
|
||||
case .invitationRejected:
|
||||
return memberIsYou ? ElementL10n.noticeRoomRejectByYou : ElementL10n.noticeRoomReject(member)
|
||||
case .invitationRevoked:
|
||||
return senderIsYou ? ElementL10n.noticeRoomThirdPartyRevokedInviteByYou(member) : ElementL10n.noticeRoomThirdPartyRevokedInvite(sender, member)
|
||||
case .knocked:
|
||||
return memberIsYou ? ElementL10n.noticeRoomKnockByYou : ElementL10n.noticeRoomKnock(member)
|
||||
case .knockAccepted:
|
||||
return senderIsYou ? ElementL10n.noticeRoomKnockAcceptedByYou(senderName) : ElementL10n.noticeRoomKnockAccepted(senderName, member)
|
||||
case .knockRetracted:
|
||||
return memberIsYou ? ElementL10n.noticeRoomKnockRetractedByYou : ElementL10n.noticeRoomKnockRetracted(member)
|
||||
case .knockDenied:
|
||||
if senderIsYou {
|
||||
return ElementL10n.noticeRoomKnockDeniedByYou(member)
|
||||
} else if memberIsYou {
|
||||
return ElementL10n.noticeRoomKnockDeniedYou(senderName)
|
||||
} else {
|
||||
return ElementL10n.noticeRoomKnockDenied(senderName, member)
|
||||
}
|
||||
case .profileChanged(let displayName, let previousDisplayName, let avatarURLString, let previousAvatarURLString):
|
||||
return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName,
|
||||
avatarURLString: avatarURLString, previousAvatarURLString: previousAvatarURLString,
|
||||
member: member, memberIsYou: memberIsYou,
|
||||
sender: sender, senderIsYou: senderIsYou)
|
||||
case .none, .error, .notImplemented, .unknown: // Not useful information for the user.
|
||||
MXLog.verbose("Filtering timeline item for membership change: \(change)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity function_parameter_count
|
||||
private func profileChangedString(displayName: String?, previousDisplayName: String?,
|
||||
avatarURLString: String?, previousAvatarURLString: String?,
|
||||
member: String, memberIsYou: Bool,
|
||||
sender: TimelineItemSender, senderIsYou: Bool) -> String {
|
||||
let displayNameChanged = displayName != previousDisplayName
|
||||
let avatarChanged = avatarURLString != previousAvatarURLString
|
||||
|
||||
switch (displayNameChanged, avatarChanged, memberIsYou) {
|
||||
case (true, false, false):
|
||||
if let displayName, let previousDisplayName {
|
||||
return ElementL10n.noticeDisplayNameChangedFrom(member, displayName, previousDisplayName)
|
||||
} else if let displayName {
|
||||
return ElementL10n.noticeDisplayNameSet(member, displayName)
|
||||
} else if let previousDisplayName {
|
||||
return ElementL10n.noticeDisplayNameRemoved(member, previousDisplayName)
|
||||
} else {
|
||||
MXLog.error("The display name changed from nil to nil, shouldn't be possible.")
|
||||
return ElementL10n.noticeMemberNoChanges(member)
|
||||
}
|
||||
case (false, true, false):
|
||||
return ElementL10n.noticeAvatarUrlChanged(displayName ?? member)
|
||||
case (true, false, true):
|
||||
if let displayName, let previousDisplayName {
|
||||
return ElementL10n.noticeDisplayNameChangedFromByYou(displayName, previousDisplayName)
|
||||
} else if let displayName {
|
||||
return ElementL10n.noticeDisplayNameSetByYou(displayName)
|
||||
} else if let previousDisplayName {
|
||||
return ElementL10n.noticeDisplayNameRemovedByYou(previousDisplayName)
|
||||
} else {
|
||||
MXLog.error("The display name changed from nil to nil, shouldn't be possible.")
|
||||
return ElementL10n.noticeMemberNoChangesByYou
|
||||
}
|
||||
case (false, true, true):
|
||||
return ElementL10n.noticeAvatarUrlChangedByYou
|
||||
case (true, true, _):
|
||||
// When both have changed, get the string for the display name and tack on that the avatar changed too.
|
||||
return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName,
|
||||
avatarURLString: nil, previousAvatarURLString: nil,
|
||||
member: member, memberIsYou: memberIsYou,
|
||||
sender: sender, senderIsYou: senderIsYou) + "\n" + ElementL10n.noticeAvatarChangedToo
|
||||
case (false, false, _):
|
||||
MXLog.error("Nothing changed, shouldn't be possible.")
|
||||
return ElementL10n.noticeMemberNoChangesByYou
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
func buildString(for state: OtherState, stateKey: String?, sender: TimelineItemSender, isOutgoing: Bool) -> String? {
|
||||
let senderName = sender.displayName ?? sender.id
|
||||
|
||||
switch state {
|
||||
case .roomAvatar(let url):
|
||||
switch (url, isOutgoing) {
|
||||
case (.some, false):
|
||||
return ElementL10n.noticeRoomAvatarChanged(senderName)
|
||||
case (nil, false):
|
||||
return ElementL10n.noticeRoomAvatarRemoved(senderName)
|
||||
case (.some, true):
|
||||
return ElementL10n.noticeRoomAvatarRemovedByYou
|
||||
case (nil, true):
|
||||
return ElementL10n.noticeRoomAvatarRemovedByYou
|
||||
}
|
||||
case .roomCreate:
|
||||
return isOutgoing ? ElementL10n.noticeRoomCreatedByYou : ElementL10n.noticeRoomCreated(senderName)
|
||||
case .roomEncryption:
|
||||
return ElementL10n.encryptionEnabled
|
||||
case .roomName(let name):
|
||||
switch (name, isOutgoing) {
|
||||
case (.some(let name), false):
|
||||
return ElementL10n.noticeRoomNameChanged(senderName, name)
|
||||
case (nil, false):
|
||||
return ElementL10n.noticeRoomNameRemoved(senderName)
|
||||
case (.some(let name), true):
|
||||
return ElementL10n.noticeRoomNameChangedByYou(name)
|
||||
case (nil, true):
|
||||
return ElementL10n.noticeRoomNameRemovedByYou
|
||||
}
|
||||
case .roomThirdPartyInvite(let displayName):
|
||||
guard let displayName else {
|
||||
MXLog.error("roomThirdPartyInvite undisplayable due to missing name.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if isOutgoing {
|
||||
return ElementL10n.noticeRoomThirdPartyInviteByYou(displayName)
|
||||
} else {
|
||||
return ElementL10n.noticeRoomThirdPartyInvite(senderName, displayName)
|
||||
}
|
||||
case .roomTopic(let topic):
|
||||
switch (topic, isOutgoing) {
|
||||
case (.some(let topic), false):
|
||||
return ElementL10n.noticeRoomTopicChanged(senderName, topic)
|
||||
case (nil, false):
|
||||
return ElementL10n.noticeRoomTopicRemoved(senderName)
|
||||
case (.some(let name), true):
|
||||
return ElementL10n.noticeRoomTopicChangedByYou(name)
|
||||
case (nil, true):
|
||||
return ElementL10n.noticeRoomTopicRemovedByYou
|
||||
}
|
||||
case .policyRuleRoom, .policyRuleServer, .policyRuleUser: // No strings available.
|
||||
break
|
||||
case .roomAliases, .roomCanonicalAlias: // Doesn't provide the alias.
|
||||
break
|
||||
case .roomGuestAccess, .roomHistoryVisibility: // Doesn't provide information about the change.
|
||||
break
|
||||
case .roomJoinRules: // Doesn't provide information about the change.
|
||||
break
|
||||
case .roomPinnedEvents, .roomPowerLevels, .roomServerAcl: // Doesn't provide information about the change.
|
||||
break
|
||||
case .roomTombstone: // Handle as a virtual timeline item with a link to the upgraded room.
|
||||
break
|
||||
case .spaceChild, .spaceParent: // Users shouldn't see the timeline of a Space.
|
||||
break
|
||||
case .custom: // Won't provide actionable information to the user.
|
||||
break
|
||||
}
|
||||
|
||||
MXLog.verbose("Filtering timeline item for state: \(state)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,24 @@ import UIKit
|
||||
struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
private let roomStateStringBuilder: RoomStateStringBuilder
|
||||
|
||||
/// The Matrix ID of the current user.
|
||||
private let userID: String
|
||||
|
||||
init(userID: String,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
attributedStringBuilder: AttributedStringBuilderProtocol,
|
||||
roomStateStringBuilder: RoomStateStringBuilder) {
|
||||
self.userID = userID
|
||||
self.mediaProvider = mediaProvider
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
self.roomStateStringBuilder = roomStateStringBuilder
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol? {
|
||||
var sender = eventItemProxy.sender
|
||||
if let senderAvatarURL = eventItemProxy.sender.avatarURL,
|
||||
let image = mediaProvider.imageFromURL(senderAvatarURL, avatarSize: .user(on: .timeline)) {
|
||||
@@ -79,14 +82,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
case .none:
|
||||
return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState)
|
||||
}
|
||||
case .state:
|
||||
return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState)
|
||||
case .roomMembership:
|
||||
return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState)
|
||||
case .state(let stateKey, let content):
|
||||
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing)
|
||||
case .roomMembership(userId: let userID, change: let change):
|
||||
return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, membershipChange: change, sender: sender, isOutgoing: isOutgoing)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
// MARK: - Message Events
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
@@ -359,4 +362,34 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
return AggregatedReaction(key: reaction.key, count: Int(reaction.count), isHighlighted: isHighlighted)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State Events
|
||||
|
||||
private func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
state: OtherState,
|
||||
stateKey: String,
|
||||
sender: TimelineItemSender,
|
||||
isOutgoing: Bool) -> RoomTimelineItemProtocol? {
|
||||
guard let text = roomStateStringBuilder.buildString(for: state, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing)
|
||||
}
|
||||
|
||||
private func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
member: String,
|
||||
membershipChange: MembershipChange,
|
||||
sender: TimelineItemSender,
|
||||
isOutgoing: Bool) -> RoomTimelineItemProtocol? {
|
||||
guard let text = roomStateStringBuilder.buildString(for: membershipChange, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing)
|
||||
}
|
||||
|
||||
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
StateRoomTimelineItem(id: eventItemProxy.id,
|
||||
text: text,
|
||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||
groupState: .single,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: false,
|
||||
sender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,5 +19,5 @@ import Foundation
|
||||
@MainActor
|
||||
protocol RoomTimelineItemFactoryProtocol {
|
||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol
|
||||
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol?
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ struct RoomTimelineViewFactory: RoomTimelineViewFactoryProtocol {
|
||||
return .unsupported(item)
|
||||
case let item as TimelineStartRoomTimelineItem:
|
||||
return .timelineStart(item)
|
||||
case let item as StateRoomTimelineItem:
|
||||
return .state(item)
|
||||
default:
|
||||
fatalError("Unknown timeline item")
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ enum RoomTimelineViewProvider: Identifiable, Hashable {
|
||||
case sticker(StickerRoomTimelineItem)
|
||||
case unsupported(UnsupportedRoomTimelineItem)
|
||||
case timelineStart(TimelineStartRoomTimelineItem)
|
||||
case state(StateRoomTimelineItem)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -63,6 +64,8 @@ enum RoomTimelineViewProvider: Identifiable, Hashable {
|
||||
return item.id
|
||||
case .timelineStart(let item):
|
||||
return item.id
|
||||
case .state(let item):
|
||||
return item.id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +101,8 @@ extension RoomTimelineViewProvider: View {
|
||||
UnsupportedRoomTimelineView(timelineItem: item)
|
||||
case .timelineStart(let item):
|
||||
TimelineStartRoomTimelineView(timelineItem: item)
|
||||
case .state(let item):
|
||||
StateRoomTimelineView(timelineItem: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,11 +149,12 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
MXLog.error("Invalid room identifier: \(roomIdentifier)")
|
||||
return
|
||||
}
|
||||
let userId = userSession.clientProxy.userIdentifier
|
||||
let userId = userSession.clientProxy.userID
|
||||
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
attributedStringBuilder: AttributedStringBuilder(),
|
||||
roomStateStringBuilder: RoomStateStringBuilder(userID: userId))
|
||||
|
||||
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(userId: userId,
|
||||
roomProxy: roomProxy,
|
||||
|
||||
@@ -78,13 +78,13 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
return .failure(.failedRefreshingRestoreToken)
|
||||
}
|
||||
|
||||
keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userIdentifier)
|
||||
keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userID)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func logout(userSession: UserSessionProtocol) {
|
||||
let userID = userSession.clientProxy.userIdentifier
|
||||
let userID = userSession.clientProxy.userID
|
||||
keychainController.removeRestorationTokenForUsername(userID)
|
||||
deleteSessionDirectory(for: userID)
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class MockScreen: Identifiable {
|
||||
userNotificationController: MockUserNotificationController(),
|
||||
isModallyPresented: false))
|
||||
case .analyticsPrompt:
|
||||
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
|
||||
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider())))
|
||||
case .authenticationFlow:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
@@ -108,7 +108,7 @@ class MockScreen: Identifiable {
|
||||
return TemplateCoordinator(parameters: .init(promptType: .upgrade))
|
||||
case .home:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let session = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:matrix.org"),
|
||||
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session,
|
||||
attributedStringBuilder: AttributedStringBuilder(),
|
||||
@@ -120,7 +120,7 @@ class MockScreen: Identifiable {
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = SettingsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userNotificationController: MockUserNotificationController(),
|
||||
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
|
||||
userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider()),
|
||||
bugReportService: MockBugReportService()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -260,7 +260,7 @@ class MockScreen: Identifiable {
|
||||
case .userSessionScreen:
|
||||
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
|
||||
|
||||
let clientProxy = MockClientProxy(userIdentifier: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded))
|
||||
let clientProxy = MockClientProxy(userID: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded))
|
||||
|
||||
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()),
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
|
||||
@@ -23,7 +23,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
var context: HomeScreenViewModelType.Context!
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
|
||||
viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider()),
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
context = viewModel.context
|
||||
|
||||
@@ -206,7 +206,7 @@ class LoggingTests: XCTestCase {
|
||||
XCTAssertFalse(content.contains(lastMessage))
|
||||
}
|
||||
|
||||
// swiftlint:disable function_body_length
|
||||
// swiftlint:disable:next function_body_length
|
||||
func testTimelineContentIsRedacted() throws {
|
||||
// Given timeline items that contain text
|
||||
let textAttributedString = "TextAttributed"
|
||||
@@ -275,8 +275,6 @@ class LoggingTests: XCTestCase {
|
||||
XCTAssertTrue(content.contains(fileMessage.id))
|
||||
XCTAssertFalse(content.contains(fileMessage.text))
|
||||
}
|
||||
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
func testRustMessageContentIsRedacted() throws {
|
||||
// Given message content that contain text
|
||||
|
||||
@@ -21,7 +21,7 @@ import Combine
|
||||
|
||||
final class NotificationManagerTests: XCTestCase {
|
||||
var notificationManager: NotificationManager!
|
||||
private let clientProxy = MockClientProxy(userIdentifier: "@test:user.net")
|
||||
private let clientProxy = MockClientProxy(userID: "@test:user.net")
|
||||
private let notificationCenter = UserNotificationCenterSpy()
|
||||
private var authorizationStatusWasGranted = false
|
||||
private var shouldDisplayInAppNotificationReturnValue = false
|
||||
|
||||
@@ -24,7 +24,7 @@ class SettingsViewModelTests: XCTestCase {
|
||||
var context: SettingsViewModelType.Context!
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: ""),
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: ""),
|
||||
mediaProvider: MockMediaProvider())
|
||||
viewModel = SettingsViewModel(withUserSession: userSession)
|
||||
context = viewModel.context
|
||||
|
||||
@@ -19,7 +19,7 @@ import XCTest
|
||||
|
||||
final class UserSessionTests: XCTestCase {
|
||||
var userSession: UserSession!
|
||||
let clientProxy = MockClientProxy(userIdentifier: "@test:user.net")
|
||||
let clientProxy = MockClientProxy(userID: "@test:user.net")
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Show state events in the timeline and (at least temporarily) on the home screen.
|
||||
Reference in New Issue
Block a user