mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 04:18:31 +00:00
Thread Decorator (#1686)
* just added the isThreaded property to message based events * done but I should add a bunch of tests for images and videos and locations * handling for image and videos, but locations might need to be double checked * code improvement * better tests and removed code that is not used, we need to fix first the appearence of replies * bubble style is solved, we now need plain style * also added plain style * better handling for bubble style * rounded corner only for bubbles in location * fix for wrong aspect ratio in replies * all changes done * better testing * swift format * changelog * PR comments and rebase. * Simplify corner radius on location timeline view. --------- Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
@@ -443,6 +443,7 @@
|
|||||||
9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */; };
|
9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */; };
|
||||||
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; };
|
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; };
|
||||||
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
|
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
|
||||||
|
915C0DB82AB3284300C2C3F6 /* TimelineMediaFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915C0DB72AB3284300C2C3F6 /* TimelineMediaFrame.swift */; };
|
||||||
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; };
|
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; };
|
||||||
92133B170A1F917685E9FF78 /* OnboardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */; };
|
92133B170A1F917685E9FF78 /* OnboardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */; };
|
||||||
9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; };
|
9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; };
|
||||||
@@ -523,6 +524,9 @@
|
|||||||
A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; };
|
A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; };
|
||||||
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
|
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
|
||||||
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; };
|
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; };
|
||||||
|
A762491F2AAF6EA800ADB9E6 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A762491E2AAF6EA800ADB9E6 /* CustomLayoutLabelStyle.swift */; };
|
||||||
|
A76249212AB1F56400ADB9E6 /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76249202AB1F56400ADB9E6 /* ThreadDecorator.swift */; };
|
||||||
|
A76249232AB2064800ADB9E6 /* SwipeToReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76249222AB2064800ADB9E6 /* SwipeToReplyView.swift */; };
|
||||||
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
|
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
|
||||||
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
|
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
|
||||||
A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; };
|
A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; };
|
||||||
@@ -1273,6 +1277,7 @@
|
|||||||
8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
||||||
90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
|
90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||||
90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = "<group>"; };
|
90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = "<group>"; };
|
||||||
|
915C0DB72AB3284300C2C3F6 /* TimelineMediaFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaFrame.swift; sourceTree = "<group>"; };
|
||||||
91CF6F7D08228D16BA69B63B /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
91CF6F7D08228D16BA69B63B /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
923485F85E1D765EF9D20E88 /* UserProfileCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileCell.swift; sourceTree = "<group>"; };
|
923485F85E1D765EF9D20E88 /* UserProfileCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileCell.swift; sourceTree = "<group>"; };
|
||||||
92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAuthenticationPresenter.swift; sourceTree = "<group>"; };
|
92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAuthenticationPresenter.swift; sourceTree = "<group>"; };
|
||||||
@@ -1333,6 +1338,9 @@
|
|||||||
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
|
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
|
||||||
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
|
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
|
||||||
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
|
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
|
||||||
|
A762491E2AAF6EA800ADB9E6 /* CustomLayoutLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayoutLabelStyle.swift; sourceTree = "<group>"; };
|
||||||
|
A76249202AB1F56400ADB9E6 /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = "<group>"; };
|
||||||
|
A76249222AB2064800ADB9E6 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = "<group>"; };
|
||||||
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
|
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
|
||||||
A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; };
|
A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = "<group>"; };
|
A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = "<group>"; };
|
||||||
@@ -3182,6 +3190,8 @@
|
|||||||
94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */,
|
94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */,
|
||||||
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */,
|
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */,
|
||||||
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */,
|
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */,
|
||||||
|
A76249202AB1F56400ADB9E6 /* ThreadDecorator.swift */,
|
||||||
|
A76249222AB2064800ADB9E6 /* SwipeToReplyView.swift */,
|
||||||
);
|
);
|
||||||
path = Style;
|
path = Style;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3575,6 +3585,8 @@
|
|||||||
565F1B2B300597C616B37888 /* FullscreenDialog.swift */,
|
565F1B2B300597C616B37888 /* FullscreenDialog.swift */,
|
||||||
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */,
|
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */,
|
||||||
EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */,
|
EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */,
|
||||||
|
A762491E2AAF6EA800ADB9E6 /* CustomLayoutLabelStyle.swift */,
|
||||||
|
915C0DB72AB3284300C2C3F6 /* TimelineMediaFrame.swift */,
|
||||||
);
|
);
|
||||||
path = Layout;
|
path = Layout;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -4525,6 +4537,7 @@
|
|||||||
663E198678778F7426A9B27D /* Collection.swift in Sources */,
|
663E198678778F7426A9B27D /* Collection.swift in Sources */,
|
||||||
24B7CD41342C143117ADA768 /* Comparable.swift in Sources */,
|
24B7CD41342C143117ADA768 /* Comparable.swift in Sources */,
|
||||||
0B57C2399B9E1CE5CE0D8005 /* ComposerToolbar.swift in Sources */,
|
0B57C2399B9E1CE5CE0D8005 /* ComposerToolbar.swift in Sources */,
|
||||||
|
A76249212AB1F56400ADB9E6 /* ThreadDecorator.swift in Sources */,
|
||||||
56BAB81A0D03C2EF09B86294 /* ComposerToolbarModels.swift in Sources */,
|
56BAB81A0D03C2EF09B86294 /* ComposerToolbarModels.swift in Sources */,
|
||||||
5995C63B1C61DE1373AA2BCE /* ComposerToolbarViewModel.swift in Sources */,
|
5995C63B1C61DE1373AA2BCE /* ComposerToolbarViewModel.swift in Sources */,
|
||||||
9BEA56957B3AF954E7321658 /* ComposerToolbarViewModelProtocol.swift in Sources */,
|
9BEA56957B3AF954E7321658 /* ComposerToolbarViewModelProtocol.swift in Sources */,
|
||||||
@@ -4718,6 +4731,7 @@
|
|||||||
FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */,
|
FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */,
|
||||||
652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */,
|
652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */,
|
||||||
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */,
|
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */,
|
||||||
|
915C0DB82AB3284300C2C3F6 /* TimelineMediaFrame.swift in Sources */,
|
||||||
C4C84901ABAC9B17564AB7EB /* NotificationName.swift in Sources */,
|
C4C84901ABAC9B17564AB7EB /* NotificationName.swift in Sources */,
|
||||||
C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */,
|
C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */,
|
||||||
AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */,
|
AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */,
|
||||||
@@ -4821,6 +4835,7 @@
|
|||||||
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */,
|
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */,
|
||||||
7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */,
|
7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */,
|
||||||
617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */,
|
617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */,
|
||||||
|
A76249232AB2064800ADB9E6 /* SwipeToReplyView.swift in Sources */,
|
||||||
6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */,
|
6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */,
|
||||||
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */,
|
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */,
|
||||||
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */,
|
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */,
|
||||||
@@ -4893,6 +4908,7 @@
|
|||||||
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */,
|
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */,
|
||||||
B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */,
|
B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */,
|
||||||
1AB3D8563AB12635250A6A6E /* StaticLocationScreenCoordinator.swift in Sources */,
|
1AB3D8563AB12635250A6A6E /* StaticLocationScreenCoordinator.swift in Sources */,
|
||||||
|
A762491F2AAF6EA800ADB9E6 /* CustomLayoutLabelStyle.swift in Sources */,
|
||||||
DFD5AA8688A34C72D48AF3B1 /* StaticLocationScreenViewModel.swift in Sources */,
|
DFD5AA8688A34C72D48AF3B1 /* StaticLocationScreenViewModel.swift in Sources */,
|
||||||
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */,
|
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */,
|
||||||
C58E305C380D3ADDF7912180 /* StickerRoomTimelineItem.swift in Sources */,
|
C58E305C380D3ADDF7912180 /* StickerRoomTimelineItem.swift in Sources */,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
"action_react" = "React";
|
"action_react" = "React";
|
||||||
"action_remove" = "Remove";
|
"action_remove" = "Remove";
|
||||||
"action_reply" = "Reply";
|
"action_reply" = "Reply";
|
||||||
|
"action_reply_in_thread" = "Reply in thread";
|
||||||
"action_report_bug" = "Report bug";
|
"action_report_bug" = "Report bug";
|
||||||
"action_report_content" = "Report Content";
|
"action_report_content" = "Report Content";
|
||||||
"action_retry" = "Retry";
|
"action_retry" = "Retry";
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
"common_syncing" = "Syncing";
|
"common_syncing" = "Syncing";
|
||||||
"common_text" = "Text";
|
"common_text" = "Text";
|
||||||
"common_third_party_notices" = "Third-party notices";
|
"common_third_party_notices" = "Third-party notices";
|
||||||
|
"common_thread" = "Thread";
|
||||||
"common_topic" = "Topic";
|
"common_topic" = "Topic";
|
||||||
"common_topic_placeholder" = "What is this room about?";
|
"common_topic_placeholder" = "What is this room about?";
|
||||||
"common_unable_to_decrypt" = "Unable to decrypt";
|
"common_unable_to_decrypt" = "Unable to decrypt";
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ public enum L10n {
|
|||||||
public static var actionRemove: String { return L10n.tr("Localizable", "action_remove") }
|
public static var actionRemove: String { return L10n.tr("Localizable", "action_remove") }
|
||||||
/// Reply
|
/// Reply
|
||||||
public static var actionReply: String { return L10n.tr("Localizable", "action_reply") }
|
public static var actionReply: String { return L10n.tr("Localizable", "action_reply") }
|
||||||
|
/// Reply in thread
|
||||||
|
public static var actionReplyInThread: String { return L10n.tr("Localizable", "action_reply_in_thread") }
|
||||||
/// Report bug
|
/// Report bug
|
||||||
public static var actionReportBug: String { return L10n.tr("Localizable", "action_report_bug") }
|
public static var actionReportBug: String { return L10n.tr("Localizable", "action_report_bug") }
|
||||||
/// Report Content
|
/// Report Content
|
||||||
@@ -288,6 +290,8 @@ public enum L10n {
|
|||||||
public static var commonText: String { return L10n.tr("Localizable", "common_text") }
|
public static var commonText: String { return L10n.tr("Localizable", "common_text") }
|
||||||
/// Third-party notices
|
/// Third-party notices
|
||||||
public static var commonThirdPartyNotices: String { return L10n.tr("Localizable", "common_third_party_notices") }
|
public static var commonThirdPartyNotices: String { return L10n.tr("Localizable", "common_third_party_notices") }
|
||||||
|
/// Thread
|
||||||
|
public static var commonThread: String { return L10n.tr("Localizable", "common_thread") }
|
||||||
/// Topic
|
/// Topic
|
||||||
public static var commonTopic: String { return L10n.tr("Localizable", "common_topic") }
|
public static var commonTopic: String { return L10n.tr("Localizable", "common_topic") }
|
||||||
/// What is this room about?
|
/// What is this room about?
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
extension LabelStyle where Self == CustomLayoutLabelStyle {
|
||||||
|
/// A label style that uses an `HStack` with parameters to customise the label's layout.
|
||||||
|
static func custom(spacing: CGFloat, alignment: VerticalAlignment = .center, iconLayout: Self.IconLayout = .leading) -> Self {
|
||||||
|
CustomLayoutLabelStyle(spacing: spacing, alignment: alignment, iconLayout: iconLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomLayoutLabelStyle: LabelStyle {
|
||||||
|
let spacing: CGFloat
|
||||||
|
var alignment: VerticalAlignment
|
||||||
|
|
||||||
|
enum IconLayout { case leading, trailing }
|
||||||
|
var iconLayout: IconLayout
|
||||||
|
|
||||||
|
fileprivate init(spacing: CGFloat, alignment: VerticalAlignment, iconLayout: IconLayout) {
|
||||||
|
self.spacing = spacing
|
||||||
|
self.alignment = alignment
|
||||||
|
self.iconLayout = iconLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
HStack(alignment: alignment, spacing: spacing) {
|
||||||
|
if iconLayout == .leading {
|
||||||
|
configuration.icon
|
||||||
|
configuration.title
|
||||||
|
} else {
|
||||||
|
configuration.title
|
||||||
|
configuration.icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
/// Constrains the max height of a media item in the timeline, whilst preserving its aspect ratio.
|
||||||
|
func timelineMediaFrame(height contentHeight: CGFloat?, aspectRatio contentAspectRatio: CGFloat?) -> some View {
|
||||||
|
aspectRatio(contentAspectRatio, contentMode: .fit)
|
||||||
|
.frame(maxHeight: min(300, max(100, contentHeight ?? .infinity)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,8 +122,17 @@ struct ComposerToolbar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var placeholder: String {
|
||||||
|
switch context.viewState.composerMode {
|
||||||
|
case .reply(_, _, let isThread):
|
||||||
|
return isThread ? L10n.actionReplyInThread : L10n.richTextEditorComposerPlaceholder
|
||||||
|
default:
|
||||||
|
return L10n.richTextEditorComposerPlaceholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var composerView: WysiwygComposerView {
|
private var composerView: WysiwygComposerView {
|
||||||
WysiwygComposerView(placeholder: L10n.richTextEditorComposerPlaceholder,
|
WysiwygComposerView(placeholder: placeholder,
|
||||||
viewModel: wysiwygViewModel,
|
viewModel: wysiwygViewModel,
|
||||||
itemProviderHelper: ItemProviderHelper(),
|
itemProviderHelper: ItemProviderHelper(),
|
||||||
keyCommandHandler: keyCommandHandler) { provider in
|
keyCommandHandler: keyCommandHandler) { provider in
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ struct MessageComposer: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .reply(_, let replyDetails):
|
case .reply(_, let replyDetails, _):
|
||||||
MessageComposerReplyHeader(replyDetails: replyDetails, action: replyCancellationAction)
|
MessageComposerReplyHeader(replyDetails: replyDetails, action: replyCancellationAction)
|
||||||
case .edit:
|
case .edit:
|
||||||
MessageComposerEditHeader(action: editCancellationAction)
|
MessageComposerEditHeader(action: editCancellationAction)
|
||||||
@@ -176,6 +176,22 @@ private struct MessageComposerHeaderLabelStyle: LabelStyle {
|
|||||||
|
|
||||||
struct MessageComposer_Previews: PreviewProvider {
|
struct MessageComposer_Previews: PreviewProvider {
|
||||||
static let viewModel = RoomScreenViewModel.mock
|
static let viewModel = RoomScreenViewModel.mock
|
||||||
|
|
||||||
|
static let replyTypes: [TimelineItemReplyDetails] = [
|
||||||
|
.loaded(sender: .init(id: "Dave"), contentType: .audio(.init(body: "Audio: Ride the lightning", duration: 100, source: nil, contentType: nil))),
|
||||||
|
.loaded(sender: .init(id: "James"), contentType: .emote(.init(body: "Emote: James thinks he's the phantom lord"))),
|
||||||
|
.loaded(sender: .init(id: "Robert"), contentType: .file(.init(body: "File: Crash course in brain surgery.pdf", source: nil, thumbnailSource: nil, contentType: nil))),
|
||||||
|
.loaded(sender: .init(id: "Cliff"), contentType: .image(.init(body: "Image: Pushead",
|
||||||
|
source: .init(url: .picturesDirectory, mimeType: nil),
|
||||||
|
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))),
|
||||||
|
.loaded(sender: .init(id: "Jason"), contentType: .notice(.init(body: "Notice: Too far gone?"))),
|
||||||
|
.loaded(sender: .init(id: "Kirk"), contentType: .text(.init(body: "Text: Where the wild things are"))),
|
||||||
|
.loaded(sender: .init(id: "Lars"), contentType: .video(.init(body: "Video: Through the never",
|
||||||
|
duration: 100,
|
||||||
|
source: nil,
|
||||||
|
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))),
|
||||||
|
.loading(eventID: "")
|
||||||
|
]
|
||||||
|
|
||||||
static func messageComposer(_ content: String = "",
|
static func messageComposer(_ content: String = "",
|
||||||
sendingDisabled: Bool = false,
|
sendingDisabled: Bool = false,
|
||||||
@@ -210,36 +226,32 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
|
|
||||||
messageComposer(mode: .reply(itemID: .random,
|
messageComposer(mode: .reply(itemID: .random,
|
||||||
replyDetails: .loaded(sender: .init(id: "Kirk"),
|
replyDetails: .loaded(sender: .init(id: "Kirk"),
|
||||||
contentType: .text(.init(body: "Text: Where the wild things are")))))
|
contentType: .text(.init(body: "Text: Where the wild things are"))), isThread: false))
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack {
|
VStack {
|
||||||
let replyTypes: [TimelineItemReplyDetails] = [
|
|
||||||
.loaded(sender: .init(id: "Dave"), contentType: .audio(.init(body: "Audio: Ride the lightning", duration: 100, source: nil, contentType: nil))),
|
|
||||||
.loaded(sender: .init(id: "James"), contentType: .emote(.init(body: "Emote: James thinks he's the phantom lord"))),
|
|
||||||
.loaded(sender: .init(id: "Robert"), contentType: .file(.init(body: "File: Crash course in brain surgery.pdf", source: nil, thumbnailSource: nil, contentType: nil))),
|
|
||||||
.loaded(sender: .init(id: "Cliff"), contentType: .image(.init(body: "Image: Pushead",
|
|
||||||
source: .init(url: .picturesDirectory, mimeType: nil),
|
|
||||||
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))),
|
|
||||||
.loaded(sender: .init(id: "Jason"), contentType: .notice(.init(body: "Notice: Too far gone?"))),
|
|
||||||
.loaded(sender: .init(id: "Kirk"), contentType: .text(.init(body: "Text: Where the wild things are"))),
|
|
||||||
.loaded(sender: .init(id: "Lars"), contentType: .video(.init(body: "Video: Through the never",
|
|
||||||
duration: 100,
|
|
||||||
source: nil,
|
|
||||||
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))),
|
|
||||||
.loading(eventID: "")
|
|
||||||
]
|
|
||||||
|
|
||||||
ForEach(replyTypes, id: \.self) { replyDetails in
|
ForEach(replyTypes, id: \.self) { replyDetails in
|
||||||
messageComposer(mode: .reply(itemID: .random,
|
messageComposer(mode: .reply(itemID: .random,
|
||||||
replyDetails: replyDetails))
|
replyDetails: replyDetails, isThread: false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
.previewDisplayName("Replying")
|
.previewDisplayName("Replying")
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
VStack {
|
||||||
|
ForEach(replyTypes, id: \.self) { replyDetails in
|
||||||
|
messageComposer(mode: .reply(itemID: .random,
|
||||||
|
replyDetails: replyDetails, isThread: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Replying in thread")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ enum RoomScreenViewModelAction {
|
|||||||
|
|
||||||
enum RoomScreenComposerMode: Equatable {
|
enum RoomScreenComposerMode: Equatable {
|
||||||
case `default`
|
case `default`
|
||||||
case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails)
|
case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails, isThread: Bool)
|
||||||
case edit(originalItemId: TimelineItemIdentifier)
|
case edit(originalItemId: TimelineItemIdentifier)
|
||||||
|
|
||||||
var isEdit: Bool {
|
var isEdit: Bool {
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
actionsSubject.send(.composer(action: .clear))
|
actionsSubject.send(.composer(action: .clear))
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case .reply(let itemId, _):
|
case .reply(let itemId, _, _):
|
||||||
await timelineController.sendMessage(message, html: html, inReplyTo: itemId)
|
await timelineController.sendMessage(message, html: html, inReplyTo: itemId)
|
||||||
case .edit(let originalItemId):
|
case .edit(let originalItemId):
|
||||||
await timelineController.editMessage(message, html: html, original: originalItemId)
|
await timelineController.editMessage(message, html: html, original: originalItemId)
|
||||||
@@ -517,8 +517,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
var actions: [TimelineItemMenuAction] = []
|
var actions: [TimelineItemMenuAction] = []
|
||||||
|
|
||||||
if item.isMessage {
|
if let messageitem = item as? EventBasedMessageTimelineItemProtocol {
|
||||||
actions.append(.reply)
|
actions.append(.reply(isThread: messageitem.isThreaded))
|
||||||
actions.append(.forward(itemID: itemID))
|
actions.append(.forward(itemID: itemID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,9 +601,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .reply:
|
case .reply:
|
||||||
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, contentType: buildReplyContent(for: eventTimelineItem))
|
let replyInfo = buildReplyInfo(for: eventTimelineItem)
|
||||||
|
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, contentType: replyInfo.type)
|
||||||
|
|
||||||
actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails))))
|
actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails, isThread: replyInfo.isThread))))
|
||||||
case .forward(let itemID):
|
case .forward(let itemID):
|
||||||
actionsSubject.send(.displayMessageForwarding(itemID: itemID))
|
actionsSubject.send(.displayMessageForwarding(itemID: itemID))
|
||||||
case .viewSource:
|
case .viewSource:
|
||||||
@@ -681,12 +682,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildReplyContent(for item: EventBasedTimelineItemProtocol) -> EventBasedMessageTimelineItemContentType {
|
private func buildReplyInfo(for item: EventBasedTimelineItemProtocol) -> ReplyInfo {
|
||||||
guard let messageItem = item as? EventBasedMessageTimelineItemProtocol else {
|
guard let messageItem = item as? EventBasedMessageTimelineItemProtocol else {
|
||||||
return .text(.init(body: item.body))
|
return .init(type: .text(.init(body: item.body)), isThread: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageItem.contentType
|
return .init(type: messageItem.contentType, isThread: messageItem.isThreaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleTappedUser(userID: String) async {
|
private func handleTappedUser(userID: String) async {
|
||||||
@@ -863,3 +864,8 @@ extension RoomScreenViewModel {
|
|||||||
analytics: ServiceLocator.shared.analytics,
|
analytics: ServiceLocator.shared.analytics,
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ReplyInfo {
|
||||||
|
let type: EventBasedMessageTimelineItemContentType
|
||||||
|
let isThread: Bool
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// 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 SwipeToReplyView: View {
|
||||||
|
let timelineItem: RoomTimelineItemProtocol
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "arrowshape.turn.up.left")
|
||||||
|
.foregroundColor(.compound.iconPrimary)
|
||||||
|
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol,
|
||||||
|
messageTimelineItem.isThreaded {
|
||||||
|
Text(L10n.actionReplyInThread)
|
||||||
|
.font(.compound.bodyXS)
|
||||||
|
.foregroundColor(.compound.textPrimary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SwipeToReplyView_Previews: PreviewProvider {
|
||||||
|
static let timelineItem = TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||||
|
timestamp: "",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: true,
|
||||||
|
isThreaded: false, sender: .init(id: ""),
|
||||||
|
content: .init(body: ""))
|
||||||
|
static let threadedTimelineItem = TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||||
|
timestamp: "",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: true,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: ""),
|
||||||
|
content: .init(body: ""))
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
SwipeToReplyView(timelineItem: timelineItem)
|
||||||
|
SwipeToReplyView(timelineItem: threadedTimelineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import Compound
|
||||||
|
|
||||||
|
struct ThreadDecorator: View {
|
||||||
|
var body: some View {
|
||||||
|
Label {
|
||||||
|
Text(L10n.commonThread)
|
||||||
|
.foregroundColor(.compound.textPrimary)
|
||||||
|
.font(.compound.bodyXS)
|
||||||
|
} icon: {
|
||||||
|
CompoundIcon(\.threads)
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundColor(.compound.iconSecondary)
|
||||||
|
}
|
||||||
|
.labelStyle(.custom(spacing: 4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThreadDecorator_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ThreadDecorator()
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
-29
@@ -17,6 +17,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
import Compound
|
||||||
|
|
||||||
struct TimelineItemBubbledStylerView<Content: View>: View {
|
struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||||
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
|
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
|
||||||
@@ -130,12 +132,12 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id))
|
context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id))
|
||||||
}
|
}
|
||||||
.swipeRightAction {
|
.swipeRightAction {
|
||||||
Image(systemName: "arrowshape.turn.up.left")
|
SwipeToReplyView(timelineItem: timelineItem)
|
||||||
.accessibilityHidden(true)
|
|
||||||
} shouldStartAction: {
|
} shouldStartAction: {
|
||||||
context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false
|
context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false
|
||||||
} action: {
|
} action: {
|
||||||
context.send(viewAction: .timelineItemMenuAction(itemID: timelineItem.id, action: .reply))
|
let isThread = (timelineItem as? EventBasedMessageTimelineItemProtocol)?.isThreaded ?? false
|
||||||
|
context.send(viewAction: .timelineItemMenuAction(itemID: timelineItem.id, action: .reply(isThread: isThread)))
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
TimelineItemMacContextMenu(item: timelineItem,
|
TimelineItemMacContextMenu(item: timelineItem,
|
||||||
@@ -217,28 +219,35 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var contentWithReply: some View {
|
var contentWithReply: some View {
|
||||||
TimelineBubbleLayout(spacing: 8) {
|
TimelineBubbleLayout(spacing: 8) {
|
||||||
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol,
|
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol {
|
||||||
let replyDetails = messageTimelineItem.replyDetails {
|
if messageTimelineItem.isThreaded {
|
||||||
// The rendered reply bubble with a greedy width. The custom layout prevents
|
ThreadDecorator()
|
||||||
// the infinite width from increasing the overall width of the view.
|
.padding(.leading, 4)
|
||||||
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
}
|
||||||
.padding(4.0)
|
if let replyDetails = messageTimelineItem.replyDetails {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
// The rendered reply bubble with a greedy width. The custom layout prevents
|
||||||
.background(Color.compound.bgCanvasDefault)
|
// the infinite width from increasing the overall width of the view.
|
||||||
.cornerRadius(8)
|
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
||||||
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(4.0)
|
||||||
// Add a fixed width reply bubble that is used for layout calculations but won't be rendered.
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
.background(Color.compound.bgCanvasDefault)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.cornerRadius(8)
|
||||||
.padding(4.0)
|
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
|
||||||
.layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote)
|
|
||||||
.hidden()
|
// Add a fixed width reply bubble that is used for layout calculations but won't be rendered.
|
||||||
|
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(4.0)
|
||||||
|
.layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote)
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content()
|
content()
|
||||||
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
|
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
|
||||||
|
.cornerRadius(timelineItem.contentCornerRadius)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,9 +321,15 @@ private extension EventBasedTimelineItemProtocol {
|
|||||||
let defaultColor: Color = isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming
|
let defaultColor: Color = isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case is ImageRoomTimelineItem,
|
case let self as EventBasedMessageTimelineItemProtocol:
|
||||||
is VideoRoomTimelineItem,
|
switch self {
|
||||||
is StickerRoomTimelineItem:
|
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
|
||||||
|
// In case a reply detail or a thread decorator is present we render the color and the padding
|
||||||
|
return self.replyDetails != nil || self.isThreaded ? defaultColor : nil
|
||||||
|
default:
|
||||||
|
return defaultColor
|
||||||
|
}
|
||||||
|
case is StickerRoomTimelineItem:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return defaultColor
|
return defaultColor
|
||||||
@@ -327,14 +342,24 @@ private extension EventBasedTimelineItemProtocol {
|
|||||||
let defaultInsets: EdgeInsets = .init(around: 8)
|
let defaultInsets: EdgeInsets = .init(around: 8)
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case is ImageRoomTimelineItem,
|
case is StickerRoomTimelineItem:
|
||||||
is VideoRoomTimelineItem,
|
|
||||||
is StickerRoomTimelineItem:
|
|
||||||
return .zero
|
return .zero
|
||||||
case is PollRoomTimelineItem:
|
case is PollRoomTimelineItem:
|
||||||
return .init(top: 12, leading: 12, bottom: 4, trailing: 12)
|
return .init(top: 12, leading: 12, bottom: 4, trailing: 12)
|
||||||
case let locationTimelineItem as LocationRoomTimelineItem:
|
case let self as EventBasedMessageTimelineItemProtocol:
|
||||||
return locationTimelineItem.content.geoURI == nil ? defaultInsets : .zero
|
switch self {
|
||||||
|
// In case a reply detail or a thread decorator is present we render the color and the padding
|
||||||
|
case is ImageRoomTimelineItem,
|
||||||
|
is VideoRoomTimelineItem:
|
||||||
|
return self.replyDetails != nil ||
|
||||||
|
self.isThreaded ? defaultInsets : .zero
|
||||||
|
case let locationTimelineItem as LocationRoomTimelineItem:
|
||||||
|
return locationTimelineItem.content.geoURI == nil ||
|
||||||
|
self.replyDetails != nil ||
|
||||||
|
self.isThreaded ? defaultInsets : .zero
|
||||||
|
default:
|
||||||
|
return defaultInsets
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return defaultInsets
|
return defaultInsets
|
||||||
}
|
}
|
||||||
@@ -358,6 +383,17 @@ private extension EventBasedTimelineItemProtocol {
|
|||||||
return defaultTimestampLayout
|
return defaultTimestampLayout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contentCornerRadius: CGFloat {
|
||||||
|
guard let message = self as? EventBasedMessageTimelineItemProtocol else { return .zero }
|
||||||
|
|
||||||
|
switch message {
|
||||||
|
case is ImageRoomTimelineItem, is VideoRoomTimelineItem, is LocationRoomTimelineItem:
|
||||||
|
return message.replyDetails != nil || message.isThreaded ? 8 : .zero
|
||||||
|
default:
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
||||||
@@ -374,6 +410,80 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
|||||||
.previewDisplayName("Mock Timeline RTL")
|
.previewDisplayName("Mock Timeline RTL")
|
||||||
replies
|
replies
|
||||||
.previewDisplayName("Replies")
|
.previewDisplayName("Replies")
|
||||||
|
threads
|
||||||
|
.previewDisplayName("Thread decorator")
|
||||||
|
}
|
||||||
|
|
||||||
|
// These akwats include a reply
|
||||||
|
static var threads: some View {
|
||||||
|
ScrollView {
|
||||||
|
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: "whoever"),
|
||||||
|
content: .init(body: "A long message that should be on multiple lines."),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))), groupStyle: .single))
|
||||||
|
|
||||||
|
AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: ""),
|
||||||
|
content: .init(body: "audio.ogg",
|
||||||
|
duration: 100,
|
||||||
|
source: nil,
|
||||||
|
contentType: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: ""),
|
||||||
|
content: .init(body: "File",
|
||||||
|
source: nil,
|
||||||
|
thumbnailSource: nil,
|
||||||
|
contentType: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: true,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: ""),
|
||||||
|
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||||
|
timestamp: "Now",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: "Bob"),
|
||||||
|
content: .init(body: "Fallback geo uri description",
|
||||||
|
geoURI: .init(latitude: 41.902782,
|
||||||
|
longitude: 12.496366),
|
||||||
|
description: "Location description description description description description description description description"),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||||
|
timestamp: "Now",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: "Bob"),
|
||||||
|
content: .init(body: "Fallback geo uri description",
|
||||||
|
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mockTimeline: some View {
|
static var mockTimeline: some View {
|
||||||
@@ -396,6 +506,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
|||||||
timestamp: "10:42",
|
timestamp: "10:42",
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "whoever"),
|
sender: .init(id: "whoever"),
|
||||||
content: .init(body: "A long message that should be on multiple lines."),
|
content: .init(body: "A long message that should be on multiple lines."),
|
||||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
@@ -405,6 +516,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
|||||||
timestamp: "10:42",
|
timestamp: "10:42",
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "whoever"),
|
sender: .init(id: "whoever"),
|
||||||
content: .init(body: "Short message"),
|
content: .init(body: "Short message"),
|
||||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
|||||||
@@ -48,17 +48,22 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var contentWithReply: some View {
|
var contentWithReply: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol,
|
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol {
|
||||||
let replyDetails = messageTimelineItem.replyDetails {
|
if messageTimelineItem.isThreaded {
|
||||||
HStack(spacing: 4.0) {
|
ThreadDecorator()
|
||||||
Rectangle()
|
}
|
||||||
.foregroundColor(.global.melon)
|
if let replyDetails = messageTimelineItem.replyDetails {
|
||||||
.frame(width: 4.0)
|
HStack(spacing: 4.0) {
|
||||||
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
Rectangle()
|
||||||
|
.foregroundColor(.global.melon)
|
||||||
|
.frame(width: 4.0)
|
||||||
|
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content()
|
content()
|
||||||
|
.layoutPriority(1)
|
||||||
}
|
}
|
||||||
.onTapGesture(count: 2) {
|
.onTapGesture(count: 2) {
|
||||||
context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id))
|
context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id))
|
||||||
@@ -72,11 +77,12 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
|||||||
context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id))
|
context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id))
|
||||||
}
|
}
|
||||||
.swipeRightAction {
|
.swipeRightAction {
|
||||||
Image(systemName: "arrowshape.turn.up.left")
|
SwipeToReplyView(timelineItem: timelineItem)
|
||||||
} shouldStartAction: {
|
} shouldStartAction: {
|
||||||
context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false
|
context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false
|
||||||
} action: {
|
} action: {
|
||||||
context.send(viewAction: .timelineItemMenuAction(itemID: timelineItem.id, action: .reply))
|
let isThread = (timelineItem as? EventBasedMessageTimelineItemProtocol)?.isThreaded ?? false
|
||||||
|
context.send(viewAction: .timelineItemMenuAction(itemID: timelineItem.id, action: .reply(isThread: isThread)))
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
TimelineItemMacContextMenu(item: timelineItem,
|
TimelineItemMacContextMenu(item: timelineItem,
|
||||||
@@ -136,6 +142,78 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
|||||||
|
|
||||||
struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
||||||
static let viewModel = RoomScreenViewModel.mock
|
static let viewModel = RoomScreenViewModel.mock
|
||||||
|
|
||||||
|
// These akwats include a reply
|
||||||
|
static var threads: some View {
|
||||||
|
ScrollView {
|
||||||
|
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: "whoever"),
|
||||||
|
content: .init(body: "A long message that should be on multiple lines."),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))), groupStyle: .single))
|
||||||
|
|
||||||
|
AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: ""),
|
||||||
|
content: .init(body: "audio.ogg",
|
||||||
|
duration: 100,
|
||||||
|
source: nil,
|
||||||
|
contentType: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: ""),
|
||||||
|
content: .init(body: "File",
|
||||||
|
source: nil,
|
||||||
|
thumbnailSource: nil,
|
||||||
|
contentType: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||||
|
timestamp: "10:42",
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: true,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: ""),
|
||||||
|
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||||
|
timestamp: "Now",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: "Bob"),
|
||||||
|
content: .init(body: "Fallback geo uri description",
|
||||||
|
geoURI: .init(latitude: 41.902782,
|
||||||
|
longitude: 12.496366),
|
||||||
|
description: "Location description description description description description description description description"),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||||
|
timestamp: "Now",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: "Bob"),
|
||||||
|
content: .init(body: "Fallback geo uri description",
|
||||||
|
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
contentType: .text(.init(body: "Short")))))
|
||||||
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
}
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
@@ -148,5 +226,9 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
|||||||
.environment(\.timelineStyle, .plain)
|
.environment(\.timelineStyle, .plain)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
|
threads
|
||||||
|
.padding()
|
||||||
|
.environment(\.timelineStyle, .plain)
|
||||||
|
.previewDisplayName("Threads")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ struct TimelineStyler<Content: View>: View {
|
|||||||
struct TimelineItemStyler_Previews: PreviewProvider {
|
struct TimelineItemStyler_Previews: PreviewProvider {
|
||||||
static let viewModel = RoomScreenViewModel.mock
|
static let viewModel = RoomScreenViewModel.mock
|
||||||
|
|
||||||
static let base = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
static let base = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
|
|
||||||
static let sentNonLast: TextRoomTimelineItem = {
|
static let sentNonLast: TextRoomTimelineItem = {
|
||||||
var result = base
|
var result = base
|
||||||
@@ -54,7 +54,7 @@ struct TimelineItemStyler_Previews: PreviewProvider {
|
|||||||
|
|
||||||
static let sendingLast: TextRoomTimelineItem = {
|
static let sendingLast: TextRoomTimelineItem = {
|
||||||
let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString
|
let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString
|
||||||
var result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
var result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
result.properties.deliveryStatus = .sending
|
result.properties.deliveryStatus = .sending
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
@@ -67,21 +67,21 @@ struct TimelineItemStyler_Previews: PreviewProvider {
|
|||||||
|
|
||||||
static let sentLast: TextRoomTimelineItem = {
|
static let sentLast: TextRoomTimelineItem = {
|
||||||
let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString
|
let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString
|
||||||
let result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
let result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static let ltrString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!"))
|
static let ltrString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!"))
|
||||||
|
|
||||||
static let rtlString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!"))
|
static let rtlString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!"))
|
||||||
|
|
||||||
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת! -- house!"))
|
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת! -- house!"))
|
||||||
|
|
||||||
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת! -- house! -- באמת!"))
|
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת! -- house! -- באמת!"))
|
||||||
|
|
||||||
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!"))
|
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!"))
|
||||||
|
|
||||||
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת! -- house!"))
|
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת! -- house!"))
|
||||||
|
|
||||||
static var testView: some View {
|
static var testView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
|||||||
+3
-1
@@ -81,7 +81,9 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider {
|
|||||||
TextRoomTimelineItem(id: .random,
|
TextRoomTimelineItem(id: .random,
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
|
sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
|
||||||
properties: .init(orderedReadReceipts: receipts))
|
properties: .init(orderedReadReceipts: receipts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ struct SwipeRightAction<Label: View>: ViewModifier {
|
|||||||
.overlay(alignment: .leading) {
|
.overlay(alignment: .leading) {
|
||||||
// We want the action icon to follow the view translation and gradually fade in
|
// We want the action icon to follow the view translation and gradually fade in
|
||||||
label()
|
label()
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: actionThreshold)
|
||||||
.opacity(xOffset / 50)
|
.opacity(xOffset / 50)
|
||||||
.animation(.interactiveSpring().speed(0.5), value: xOffset)
|
.animation(.interactiveSpring().speed(0.5), value: xOffset)
|
||||||
.offset(x: -actionThreshold + min(xOffset, actionThreshold), y: 0.0)
|
.offset(x: -actionThreshold + min(xOffset, actionThreshold), y: 0.0)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ struct AudioRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "audio.ogg", duration: 300, source: nil, contentType: nil)))
|
content: .init(body: "audio.ogg", duration: 300, source: nil, contentType: nil)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: senderId),
|
sender: .init(id: senderId),
|
||||||
content: .init(body: text))
|
content: .init(body: text))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "document.pdf", source: nil, thumbnailSource: nil, contentType: nil)))
|
content: .init(body: "document.pdf", source: nil, thumbnailSource: nil, contentType: nil)))
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "document.docx", source: nil, thumbnailSource: nil, contentType: nil)))
|
content: .init(body: "document.docx", source: nil, thumbnailSource: nil, contentType: nil)))
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "document.txt", source: nil, thumbnailSource: nil, contentType: nil)))
|
content: .init(body: "document.txt", source: nil, thumbnailSource: nil, contentType: nil)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ struct ImageRoomTimelineView: View {
|
|||||||
imageProvider: context.imageProvider) {
|
imageProvider: context.imageProvider) {
|
||||||
placeholder
|
placeholder
|
||||||
}
|
}
|
||||||
.frame(maxHeight: min(300, max(100, timelineItem.content.height ?? .infinity)))
|
.timelineMediaFrame(height: timelineItem.content.height,
|
||||||
.aspectRatio(timelineItem.content.aspectRatio, contentMode: .fit)
|
aspectRatio: timelineItem.content.height)
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel(L10n.commonImage)
|
.accessibilityLabel(L10n.commonImage)
|
||||||
}
|
}
|
||||||
@@ -72,6 +72,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
|
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
|
content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Blurhashed image",
|
content: .init(body: "Blurhashed image",
|
||||||
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"),
|
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"),
|
||||||
|
|||||||
@@ -86,20 +86,33 @@ struct LocationRoomTimelineView_Previews: PreviewProvider {
|
|||||||
static let viewModel = RoomScreenViewModel.mock
|
static let viewModel = RoomScreenViewModel.mock
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
body
|
ScrollView {
|
||||||
.environmentObject(viewModel.context)
|
VStack {
|
||||||
|
states
|
||||||
body
|
.padding(.horizontal)
|
||||||
.environment(\.timelineStyle, .plain)
|
}
|
||||||
.environmentObject(viewModel.context)
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Bubbles")
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
VStack {
|
||||||
|
states
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environment(\.timelineStyle, .plain)
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Plain")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
static var body: some View {
|
static var states: some View {
|
||||||
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Fallback geo uri description")))
|
content: .init(body: "Fallback geo uri description")))
|
||||||
|
|
||||||
@@ -107,8 +120,19 @@ struct LocationRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Fallback geo uri description",
|
content: .init(body: "Fallback geo uri description",
|
||||||
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description")))
|
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description")))
|
||||||
|
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||||
|
timestamp: "Now",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
isThreaded: true,
|
||||||
|
sender: .init(id: "Bob"),
|
||||||
|
content: .init(body: "Fallback geo uri description",
|
||||||
|
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description"),
|
||||||
|
replyDetails: .loaded(sender: .init(id: "Someone"),
|
||||||
|
contentType: .text(.init(body: "The thread content goes 'ere.")))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: senderId),
|
sender: .init(id: senderId),
|
||||||
content: .init(body: text))
|
content: .init(body: text))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "1", displayName: "Bob"),
|
sender: .init(id: "1", displayName: "Bob"),
|
||||||
content: .init(body: "This is another message"))), groupStyle: .single))
|
content: .init(body: "This is another message"))), groupStyle: .single))
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Alice"),
|
sender: .init(id: "", displayName: "Alice"),
|
||||||
content: .init(body: "This is a message"))), groupStyle: .single))
|
content: .init(body: "This is a message"))), groupStyle: .single))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ struct StickerRoomTimelineView: View {
|
|||||||
imageProvider: context.imageProvider) {
|
imageProvider: context.imageProvider) {
|
||||||
placeholder
|
placeholder
|
||||||
}
|
}
|
||||||
.frame(maxHeight: min(300, max(100, timelineItem.height ?? .infinity)))
|
.timelineMediaFrame(height: timelineItem.height,
|
||||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
aspectRatio: timelineItem.aspectRatio)
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)")
|
.accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: isOutgoing,
|
isEditable: isOutgoing,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: senderId),
|
sender: .init(id: senderId),
|
||||||
content: .init(body: text))
|
content: .init(body: text))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ struct VideoRoomTimelineView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
thumbnail
|
thumbnail
|
||||||
.frame(maxHeight: min(300, max(100, timelineItem.content.height ?? .infinity)))
|
.timelineMediaFrame(height: timelineItem.content.height,
|
||||||
.aspectRatio(timelineItem.content.aspectRatio, contentMode: .fit)
|
aspectRatio: timelineItem.content.aspectRatio)
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel(L10n.commonVideo)
|
.accessibilityLabel(L10n.commonVideo)
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Some video", duration: 21, source: nil, thumbnailSource: nil)))
|
content: .init(body: "Some video", duration: 21, source: nil, thumbnailSource: nil)))
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Some other video", duration: 22, source: nil, thumbnailSource: nil)))
|
content: .init(body: "Some other video", duration: 22, source: nil, thumbnailSource: nil)))
|
||||||
|
|
||||||
@@ -97,6 +99,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Blurhashed video", duration: 23, source: nil, thumbnailSource: nil, aspectRatio: 0.7, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")))
|
content: .init(body: "Blurhashed video", duration: 23, source: nil, thumbnailSource: nil, aspectRatio: 0.7, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
|||||||
case edit
|
case edit
|
||||||
case copyPermalink
|
case copyPermalink
|
||||||
case redact
|
case redact
|
||||||
case reply
|
case reply(isThread: Bool)
|
||||||
case forward(itemID: TimelineItemIdentifier)
|
case forward(itemID: TimelineItemIdentifier)
|
||||||
case viewSource
|
case viewSource
|
||||||
case retryDecryption(sessionID: String)
|
case retryDecryption(sessionID: String)
|
||||||
@@ -106,8 +106,8 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
|||||||
Label(L10n.actionEdit, systemImage: "pencil.line")
|
Label(L10n.actionEdit, systemImage: "pencil.line")
|
||||||
case .copyPermalink:
|
case .copyPermalink:
|
||||||
Label(L10n.actionCopyLinkToMessage, systemImage: "link")
|
Label(L10n.actionCopyLinkToMessage, systemImage: "link")
|
||||||
case .reply:
|
case .reply(let isThread):
|
||||||
Label(L10n.actionReply, systemImage: "arrowshape.turn.up.left")
|
Label(isThread ? L10n.actionReplyInThread : L10n.actionReply, systemImage: "arrowshape.turn.up.left")
|
||||||
case .forward:
|
case .forward:
|
||||||
Label(L10n.actionForward, systemImage: "arrowshape.turn.up.right")
|
Label(L10n.actionForward, systemImage: "arrowshape.turn.up.right")
|
||||||
case .redact:
|
case .redact:
|
||||||
@@ -292,7 +292,7 @@ struct TimelineItemMenu_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if let item = RoomTimelineItemFixtures.singleMessageChunk.first as? EventBasedTimelineItemProtocol,
|
if let item = RoomTimelineItemFixtures.singleMessageChunk.first as? EventBasedTimelineItemProtocol,
|
||||||
let actions = TimelineItemMenuActions(actions: [.copy, .edit, .reply, .redact], debugActions: [.viewSource]) {
|
let actions = TimelineItemMenuActions(actions: [.copy, .edit, .reply(isThread: false), .redact], debugActions: [.viewSource]) {
|
||||||
TimelineItemMenu(item: item, actions: actions)
|
TimelineItemMenu(item: item, actions: actions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
timestamp: "10:10 AM",
|
timestamp: "10:10 AM",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Jacob"),
|
sender: .init(id: "", displayName: "Jacob"),
|
||||||
content: .init(body: "That looks so good!"),
|
content: .init(body: "That looks so good!"),
|
||||||
properties: RoomTimelineItemProperties(isEdited: true)),
|
properties: RoomTimelineItemProperties(isEdited: true)),
|
||||||
@@ -31,6 +32,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
timestamp: "10:11 AM",
|
timestamp: "10:11 AM",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
content: .init(body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗"),
|
content: .init(body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗"),
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
@@ -40,6 +42,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
timestamp: "10:11 AM",
|
timestamp: "10:11 AM",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
content: .init(body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/"),
|
content: .init(body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/"),
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
@@ -57,6 +60,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
content: .init(body: "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!"),
|
content: .init(body: "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!"),
|
||||||
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
|
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
|
||||||
@@ -64,12 +68,14 @@ enum RoomTimelineItemFixtures {
|
|||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Bob"),
|
sender: .init(id: "", displayName: "Bob"),
|
||||||
content: .init(body: "And John's speech was amazing!")),
|
content: .init(body: "And John's speech was amazing!")),
|
||||||
TextRoomTimelineItem(id: .random,
|
TextRoomTimelineItem(id: .random,
|
||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Bob"),
|
sender: .init(id: "", displayName: "Bob"),
|
||||||
content: .init(body: "New home office set up!"),
|
content: .init(body: "New home office set up!"),
|
||||||
properties: RoomTimelineItemProperties(reactions: AggregatedReaction.mockReactions,
|
properties: RoomTimelineItemProperties(reactions: AggregatedReaction.mockReactions,
|
||||||
@@ -81,6 +87,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
content: .init(body: "",
|
content: .init(body: "",
|
||||||
formattedBody: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL).fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳")))
|
formattedBody: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL).fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳")))
|
||||||
@@ -230,6 +237,7 @@ private extension TextRoomTimelineItem {
|
|||||||
timestamp: "10:47 am",
|
timestamp: "10:47 am",
|
||||||
isOutgoing: senderDisplayName == "Alice",
|
isOutgoing: senderDisplayName == "Alice",
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "", displayName: senderDisplayName),
|
sender: .init(id: "", displayName: senderDisplayName),
|
||||||
content: .init(body: text))
|
content: .init(body: text))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -30,4 +30,5 @@ enum EventBasedMessageTimelineItemContentType: Hashable {
|
|||||||
protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
|
protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
|
||||||
var replyDetails: TimelineItemReplyDetails? { get }
|
var replyDetails: TimelineItemReplyDetails? { get }
|
||||||
var contentType: EventBasedMessageTimelineItemContentType { get }
|
var contentType: EventBasedMessageTimelineItemContentType { get }
|
||||||
|
var isThreaded: Bool { get }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -21,6 +21,7 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
let isThreaded: Bool
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
let content: AudioRoomTimelineItemContent
|
let content: AudioRoomTimelineItemContent
|
||||||
|
|||||||
+1
@@ -21,6 +21,7 @@ struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
let isThreaded: Bool
|
||||||
|
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
|
|||||||
+2
@@ -23,6 +23,8 @@ struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
|||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
let isThreaded: Bool
|
||||||
|
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
let content: FileRoomTimelineItemContent
|
let content: FileRoomTimelineItemContent
|
||||||
|
|||||||
+1
@@ -22,6 +22,7 @@ struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
let isThreaded: Bool
|
||||||
|
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,8 @@ struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatabl
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
let isThreaded: Bool
|
||||||
|
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
let content: LocationRoomTimelineItemContent
|
let content: LocationRoomTimelineItemContent
|
||||||
|
|||||||
+1
@@ -21,6 +21,7 @@ struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
let isThreaded: Bool
|
||||||
|
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
|
|||||||
+2
@@ -23,6 +23,8 @@ struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
|
|||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
let isThreaded: Bool
|
||||||
|
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
let content: TextRoomTimelineItemContent
|
let content: TextRoomTimelineItemContent
|
||||||
|
|||||||
+1
@@ -22,6 +22,7 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
let isThreaded: Bool
|
||||||
|
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|
||||||
|
|||||||
@@ -80,23 +80,24 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
fatalError("Invalid message timeline item: \(eventItemProxy)")
|
fatalError("Invalid message timeline item: \(eventItemProxy)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isThreaded = messageTimelineItem.isThreaded()
|
||||||
switch messageTimelineItem.msgtype() {
|
switch messageTimelineItem.msgtype() {
|
||||||
case .text(content: let content):
|
case .text(content: let content):
|
||||||
return buildTextTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildTextTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .image(content: let content):
|
case .image(content: let content):
|
||||||
return buildImageTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildImageTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .video(let content):
|
case .video(let content):
|
||||||
return buildVideoTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildVideoTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .file(let content):
|
case .file(let content):
|
||||||
return buildFileTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildFileTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .notice(content: let content):
|
case .notice(content: let content):
|
||||||
return buildNoticeTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildNoticeTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .emote(content: let content):
|
case .emote(content: let content):
|
||||||
return buildEmoteTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildEmoteTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .audio(let content):
|
case .audio(let content):
|
||||||
return buildAudioTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildAudioTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .location(let content):
|
case .location(let content):
|
||||||
return buildLocationTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
|
return buildLocationTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded)
|
||||||
case .none:
|
case .none:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -182,11 +183,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildTextTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildTextTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: TextMessageContent,
|
_ messageContent: TextMessageContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
TextRoomTimelineItem(id: eventItemProxy.id,
|
TextRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildTextTimelineItemContent(messageContent),
|
content: buildTextTimelineItemContent(messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
@@ -199,11 +202,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: ImageMessageContent,
|
_ messageContent: ImageMessageContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
ImageRoomTimelineItem(id: eventItemProxy.id,
|
ImageRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildImageTimelineItemContent(messageContent),
|
content: buildImageTimelineItemContent(messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
@@ -216,11 +221,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: VideoMessageContent,
|
_ messageContent: VideoMessageContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
VideoRoomTimelineItem(id: eventItemProxy.id,
|
VideoRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildVideoTimelineItemContent(messageContent),
|
content: buildVideoTimelineItemContent(messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
@@ -233,11 +240,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: AudioMessageContent,
|
_ messageContent: AudioMessageContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
AudioRoomTimelineItem(id: eventItemProxy.id,
|
AudioRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildAudioTimelineItemContent(messageContent),
|
content: buildAudioTimelineItemContent(messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
@@ -250,11 +259,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: FileMessageContent,
|
_ messageContent: FileMessageContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
FileRoomTimelineItem(id: eventItemProxy.id,
|
FileRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildFileTimelineItemContent(messageContent),
|
content: buildFileTimelineItemContent(messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
@@ -267,11 +278,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: NoticeMessageContent,
|
_ messageContent: NoticeMessageContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
NoticeRoomTimelineItem(id: eventItemProxy.id,
|
NoticeRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildNoticeTimelineItemContent(messageContent),
|
content: buildNoticeTimelineItemContent(messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
@@ -284,11 +297,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: EmoteMessageContent,
|
_ messageContent: EmoteMessageContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
EmoteRoomTimelineItem(id: eventItemProxy.id,
|
EmoteRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: messageContent),
|
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
@@ -301,11 +316,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
_ messageTimelineItem: Message,
|
_ messageTimelineItem: Message,
|
||||||
_ messageContent: LocationContent,
|
_ messageContent: LocationContent,
|
||||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
_ isOutgoing: Bool,
|
||||||
|
_ isThreaded: Bool) -> RoomTimelineItemProtocol {
|
||||||
LocationRoomTimelineItem(id: eventItemProxy.id,
|
LocationRoomTimelineItem(id: eventItemProxy.id,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
|
isThreaded: isThreaded,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildLocationTimelineItemContent(messageContent),
|
content: buildLocationTimelineItemContent(messageContent),
|
||||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ class LoggingTests: XCTestCase {
|
|||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "sender"),
|
sender: .init(id: "sender"),
|
||||||
content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString)))
|
content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString)))
|
||||||
let noticeAttributedString = "NoticeAttributed"
|
let noticeAttributedString = "NoticeAttributed"
|
||||||
@@ -264,6 +265,7 @@ class LoggingTests: XCTestCase {
|
|||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "sender"),
|
sender: .init(id: "sender"),
|
||||||
content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString)))
|
content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString)))
|
||||||
let emoteAttributedString = "EmoteAttributed"
|
let emoteAttributedString = "EmoteAttributed"
|
||||||
@@ -271,24 +273,28 @@ class LoggingTests: XCTestCase {
|
|||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "sender"),
|
sender: .init(id: "sender"),
|
||||||
content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString)))
|
content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString)))
|
||||||
let imageMessage = ImageRoomTimelineItem(id: .init(timelineID: "myimagemessage"),
|
let imageMessage = ImageRoomTimelineItem(id: .init(timelineID: "myimagemessage"),
|
||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "sender"),
|
sender: .init(id: "sender"),
|
||||||
content: .init(body: "ImageString", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), thumbnailSource: nil))
|
content: .init(body: "ImageString", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), thumbnailSource: nil))
|
||||||
let videoMessage = VideoRoomTimelineItem(id: .random,
|
let videoMessage = VideoRoomTimelineItem(id: .random,
|
||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "sender"),
|
sender: .init(id: "sender"),
|
||||||
content: .init(body: "VideoString", duration: 0, source: nil, thumbnailSource: nil))
|
content: .init(body: "VideoString", duration: 0, source: nil, thumbnailSource: nil))
|
||||||
let fileMessage = FileRoomTimelineItem(id: .random,
|
let fileMessage = FileRoomTimelineItem(id: .random,
|
||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "sender"),
|
sender: .init(id: "sender"),
|
||||||
content: .init(body: "FileString", source: nil, thumbnailSource: nil, contentType: nil))
|
content: .init(body: "FileString", source: nil, thumbnailSource: nil, contentType: nil))
|
||||||
|
|
||||||
|
|||||||
@@ -511,6 +511,7 @@ private extension TextRoomTimelineItem {
|
|||||||
timestamp: "10:47 am",
|
timestamp: "10:47 am",
|
||||||
isOutgoing: sender == "bob",
|
isOutgoing: sender == "bob",
|
||||||
isEditable: sender == "bob",
|
isEditable: sender == "bob",
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: "@\(sender):server.com", displayName: sender),
|
sender: .init(id: "@\(sender):server.com", displayName: sender),
|
||||||
content: .init(body: text),
|
content: .init(body: text),
|
||||||
properties: RoomTimelineItemProperties(reactions: reactions))
|
properties: RoomTimelineItemProperties(reactions: reactions))
|
||||||
@@ -529,6 +530,7 @@ private extension TextRoomTimelineItem {
|
|||||||
timestamp: "",
|
timestamp: "",
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
|
isThreaded: false,
|
||||||
sender: .init(id: ""),
|
sender: .init(id: ""),
|
||||||
content: .init(body: "Hello, World!"))
|
content: .init(body: "Hello, World!"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,24 +20,24 @@ import XCTest
|
|||||||
final class TextBasedRoomTimelineTests: XCTestCase {
|
final class TextBasedRoomTimelineTests: XCTestCase {
|
||||||
func testTextRoomTimelineItemWhitespaceEnd() {
|
func testTextRoomTimelineItemWhitespaceEnd() {
|
||||||
let timestamp = "Now"
|
let timestamp = "Now"
|
||||||
let timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
let timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .bubbles), timestamp.count + 1)
|
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .bubbles), timestamp.count + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTextRoomTimelineItemWhitespaceEndLonger() {
|
func testTextRoomTimelineItemWhitespaceEndLonger() {
|
||||||
let timestamp = "10:00 AM"
|
let timestamp = "10:00 AM"
|
||||||
let timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
let timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .bubbles), timestamp.count + 1)
|
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .bubbles), timestamp.count + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTextRoomTimelineItemWhitespaceEndPlain() {
|
func testTextRoomTimelineItemWhitespaceEndPlain() {
|
||||||
let timelineItem = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
let timelineItem = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: true, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .plain), 0)
|
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .plain), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTextRoomTimelineItemWhitespaceEndWithEdit() {
|
func testTextRoomTimelineItemWhitespaceEndWithEdit() {
|
||||||
let timestamp = "Now"
|
let timestamp = "Now"
|
||||||
var timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
var timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
timelineItem.properties.isEdited = true
|
timelineItem.properties.isEdited = true
|
||||||
let editedCount = L10n.commonEditedSuffix.count
|
let editedCount = L10n.commonEditedSuffix.count
|
||||||
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .bubbles), timestamp.count + editedCount + 2)
|
XCTAssertEqual(timelineItem.additionalWhitespaces(timelineStyle: .bubbles), timestamp.count + editedCount + 2)
|
||||||
@@ -45,7 +45,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
|
|||||||
|
|
||||||
func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() {
|
func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() {
|
||||||
let timestamp = "Now"
|
let timestamp = "Now"
|
||||||
var timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
var timelineItem = TextRoomTimelineItem(id: .random, timestamp: timestamp, isOutgoing: true, isEditable: true, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
|
||||||
timelineItem.properties.isEdited = true
|
timelineItem.properties.isEdited = true
|
||||||
timelineItem.properties.deliveryStatus = .sendingFailed
|
timelineItem.properties.deliveryStatus = .sendingFailed
|
||||||
let editedCount = L10n.commonEditedSuffix.count
|
let editedCount = L10n.commonEditedSuffix.count
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Messages that are part of a thread will be marked with a thread decorator.
|
||||||
Reference in New Issue
Block a user