From 4b728f3d09a9dc41c1d9566354e2b95b1a1038f0 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 11 May 2022 16:27:30 +0300 Subject: [PATCH] Update UserIndicators with newer implementation from Element (modified to work here) --- ElementX.xcodeproj/project.pbxproj | 172 +++++++++++------- ElementX/Sources/AppCoordinator.swift | 31 +--- .../FullscreenLoadingActivityPresenter.swift | 67 ------- .../ToastActivityPresenter.swift | 70 ------- .../ActivityIndicatorPresenter.swift | 148 +++++++++++++++ .../ActivityIndicatorPresenterType.swift | 34 ++++ .../ActivityIndicatorView.swift | 112 ++++++++++++ .../UserIndicators/ActivityIndicatorView.xib | 55 ++++++ .../FullscreenLoadingViewPresenter.swift | 75 ++++++++ .../LabelledActivityIndicatorView.swift | 0 .../RectangleToastView.swift | 0 .../RoundedToastView.swift | 43 ++--- .../UserIndicators/ToastViewPresenter.swift | 93 ++++++++++ .../Other/UserIndicators/ToastViewState.swift | 27 +++ .../UserIndicator.swift} | 27 +-- .../UserIndicatorDismissal.swift} | 8 +- .../UserIndicatorPresentationContext.swift | 42 +++++ .../UserIndicatorPresenter.swift | 116 ++++++++++++ .../UserIndicatorQueue.swift} | 33 ++-- .../UserIndicatorRequest.swift} | 10 +- .../UserIndicators/UserIndicatorStore.swift | 63 +++++++ .../UserIndicatorViewPresentable.swift} | 8 +- .../UserIndicatorPresenterSpy.swift | 30 +++ .../UserIndicatorQueueTests.swift | 56 ++++++ .../UserIndicators/UserIndicatorTests.swift | 128 +++++++++++++ 25 files changed, 1151 insertions(+), 297 deletions(-) delete mode 100644 ElementX/Sources/Other/Activity/ActivityPresenters/FullscreenLoadingActivityPresenter.swift delete mode 100644 ElementX/Sources/Other/Activity/ActivityPresenters/ToastActivityPresenter.swift create mode 100755 ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift create mode 100644 ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift create mode 100755 ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift create mode 100755 ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib create mode 100644 ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift rename ElementX/Sources/Other/{Activity => UserIndicators}/LabelledActivityIndicatorView.swift (100%) rename ElementX/Sources/Other/{Activity/Toasts => UserIndicators}/RectangleToastView.swift (100%) rename ElementX/Sources/Other/{Activity/Toasts => UserIndicators}/RoundedToastView.swift (78%) create mode 100644 ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift create mode 100644 ElementX/Sources/Other/UserIndicators/ToastViewState.swift rename ElementX/Sources/Other/{Activity/Activity.swift => UserIndicators/UserIndicator.swift} (63%) rename ElementX/Sources/Other/{Activity/ActivityDismissal.swift => UserIndicators/UserIndicatorDismissal.swift} (69%) create mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift create mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift rename ElementX/Sources/Other/{Activity/ActivityCenter.swift => UserIndicators/UserIndicatorQueue.swift} (52%) rename ElementX/Sources/Other/{Activity/ActivityRequest.swift => UserIndicators/UserIndicatorRequest.swift} (65%) create mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift rename ElementX/Sources/Other/{Activity/ActivityPresentable.swift => UserIndicators/UserIndicatorViewPresentable.swift} (65%) create mode 100644 UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift create mode 100644 UnitTests/Sources/UserIndicators/UserIndicatorQueueTests.swift create mode 100644 UnitTests/Sources/UserIndicators/UserIndicatorTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 0edab208c..9c1809594 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -14,13 +14,16 @@ 03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */; }; 03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */; }; 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; + 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; + 10866439ABA58CCDB5D1459D /* UserIndicatorQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */; }; + 1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */; }; 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */; }; 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */; }; - 15FEC447B7FEA62F418732AC /* ToastActivityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09C9DFFE9A897E439D770C5 /* ToastActivityPresenter.swift */; }; + 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; }; 17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */; }; 1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; @@ -37,6 +40,7 @@ 2E59008365E01F0AFB3A6B24 /* ImageRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */; }; + 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */; }; 306CC09DF101E7E9CDE79AA5 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C8F70ADAFB63907B862E5D /* LoginScreenCoordinator.swift */; }; 33912D1B9264D897033E0681 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0779B2CC9A687CBB82A5B920 /* LoginScreenViewModelProtocol.swift */; }; 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; }; @@ -46,23 +50,23 @@ 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; 39AE84C8E5F2FE9D2DC7775C /* EventBasedTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */; }; - 3C7009EB92E77136FF7BD9D7 /* FullscreenLoadingActivityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7D1C4699CE71F08DC5B016 /* FullscreenLoadingActivityPresenter.swift */; }; - 3C732330B5A3D0A64142B280 /* ActivityPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC54146B646F161762B54BBF /* ActivityPresentable.swift */; }; 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4411C0DA0087A1CB143E96FA /* EventBrief.swift */; }; 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; - 417E2190C02B96F498592559 /* ActivityDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34FB01A9AC30E2FA59678BC5 /* ActivityDismissal.swift */; }; 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */; }; + 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; + 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */; }; 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; + 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */; }; + 4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C2318DF4C0E601EEE31F84 /* ActivityIndicatorPresenterType.swift */; }; 4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */; }; 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; }; 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874A1842477895F199567BD7 /* TimelineView.swift */; }; 50391038BC50C8ED9A4D88A0 /* WeakDictionaryReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B23371BC8BF6164D9F6A05 /* WeakDictionaryReference.swift */; }; - 51910CB919287139F2DF6261 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D867D38F60E364DA470C131D /* RectangleToastView.swift */; }; 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; }; - 539F448EC0250880703775DE /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3947F320B9F5748C736AD3 /* LabelledActivityIndicatorView.swift */; }; + 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; @@ -75,23 +79,26 @@ 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; }; 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; + 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */; }; 6F2AB43A1EFAD8A97AF41A15 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; }; - 7684092532FBC71B73916441 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4238D6EB22C0F9D7A2EEC9 /* RoundedToastView.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */; }; 78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */; }; 7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */; }; + 7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; - 7C72B61B30B41D76953237A8 /* ActivityRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525FEE63EBF02C97161BF7D /* ActivityRequest.swift */; }; 7C9121245B11CA48307CB462 /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8FD25EB4DF66625B74E4505 /* LoginScreenViewModel.swift */; }; 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */; }; 7DE5EB4CB2401C672257283C /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B12969CEC0051BC750DA5068 /* WeakKeyDictionary.swift */; }; 7E1EDBA3934E6C29E5BD045B /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DD2DA5DC8654F2A80FF1D /* Bundle.swift */; }; 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47564C584F614B7287F3EB /* RootRouter.swift */; }; + 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */; }; + 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */; }; + 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; @@ -103,6 +110,8 @@ 9B8DE1D424E37581C7D99CCC /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */; }; 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; + 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */; }; + 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */; }; 9D2E03DB175A6AB14589076D /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; @@ -116,11 +125,12 @@ AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; B0887A7B5AFEC88981626389 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64839516BD56D1C81D84C5E0 /* MXLog.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; - B4EF927F0C14F2F3F8D63DE8 /* ActivityCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED659E775C4FCEBC977C6A08 /* ActivityCenter.swift */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; BE3237142FA6E1A13C0E7D11 /* RoomSummaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */; }; + BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */; }; BF35062D06888FA80BD139FF /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB7F9D6FC121204D59E18DF /* Presentable.swift */; }; + C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */; }; @@ -129,8 +139,8 @@ D013E70C8E28E43497820444 /* TextRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */; }; D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; - D735AF72894A273F53D941B8 /* Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B33C10BC91ABFF09605DB6 /* Activity.swift */; }; D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; }; + D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; }; DD4ADDB73E0935B74D2D18D6 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E3FE65EE63CBA65592863C2 /* UserSession.swift */; }; DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */; }; @@ -146,6 +156,7 @@ F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; + FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */; }; FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F395A2E917115C7AAF7F34 /* SplashViewController.swift */; }; FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA154570F693D93513E584C1 /* RoomMessageFactory.swift */; }; /* End PBXBuildFile section */ @@ -171,6 +182,7 @@ 01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 02A07FF019724B6ACEA73076 /* szl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = szl; path = szl.lproj/Localizable.strings; sourceTree = ""; }; 04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = ""; }; + 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueueTests.swift; sourceTree = ""; }; 057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = ""; }; 0779B2CC9A687CBB82A5B920 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = ""; }; 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = ""; }; @@ -194,6 +206,7 @@ 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; + 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorViewPresentable.swift; sourceTree = ""; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; 1A2082B5226B2A3A4D0798B6 /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = ""; }; 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = ""; }; @@ -216,7 +229,6 @@ 2CF9FE7E0CF9F40D1509E63A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; - 34FB01A9AC30E2FA59678BC5 /* ActivityDismissal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityDismissal.swift; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactory.swift; sourceTree = ""; }; 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -228,11 +240,13 @@ 3CDF9E55650D6035D6536538 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = ""; }; 3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = ""; }; + 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = ""; }; 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = ""; }; 4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomMessage.swift; sourceTree = ""; }; + 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenLoadingViewPresenter.swift; sourceTree = ""; }; 434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; - 43B33C10BC91ABFF09605DB6 /* Activity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Activity.swift; sourceTree = ""; }; 4411C0DA0087A1CB143E96FA /* EventBrief.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBrief.swift; sourceTree = ""; }; + 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewPresenter.swift; sourceTree = ""; }; 4488F5F92A64A137665C96CD /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Localizable.strings; sourceTree = ""; }; 44AEEE13AC1BF303AE48CBF8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 453E722A43D092C06FB8E3FA /* tzm */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tzm; path = tzm.lproj/Localizable.strings; sourceTree = ""; }; @@ -246,8 +260,9 @@ 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = ""; }; 4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = ""; }; + 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewState.swift; sourceTree = ""; }; 4C8D988E82A8DFA13BE46F7C /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pl; path = pl.lproj/Localizable.stringsdict; sourceTree = ""; }; - 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = ""; }; 4DF56C3239EA3C16951E1E66 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTableViewAdapter.swift; sourceTree = ""; }; @@ -261,19 +276,22 @@ 55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = ""; }; 55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = ""; }; 56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineView.swift; sourceTree = ""; }; + 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = ""; }; 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = ""; }; - 5B4238D6EB22C0F9D7A2EEC9 /* RoundedToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedToastView.swift; sourceTree = ""; }; 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = ""; }; 5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; + 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; 5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = ""; }; 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageProtocol.swift; sourceTree = ""; }; 616197D81103330BF2ADD559 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = ""; }; 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = ""; }; + 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorRequest.swift; sourceTree = ""; }; 61C8F70ADAFB63907B862E5D /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = ""; }; + 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleToastView.swift; sourceTree = ""; }; 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 64839516BD56D1C81D84C5E0 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; @@ -282,12 +300,13 @@ 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; + 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = ""; }; 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProvider.swift; sourceTree = ""; }; - 6B3947F320B9F5748C736AD3 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = ""; }; 6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; + 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresentationContext.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = ""; }; 71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; @@ -309,6 +328,7 @@ 8210612D17A39369480FC183 /* MediaSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSource.swift; sourceTree = ""; }; 84E92FF38EBC12EC2452C79C /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = ""; }; 858F8D0B0D51CC41BAA18E24 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; + 85C2318DF4C0E601EEE31F84 /* ActivityIndicatorPresenterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenterType.swift; sourceTree = ""; }; 85CB1DDCEE53B946D09DF4F6 /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-BD"; path = "bn-BD.lproj/Localizable.strings"; sourceTree = ""; }; 873718F8BD17B778C5141C45 /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = ""; }; 874A1842477895F199567BD7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; @@ -321,13 +341,11 @@ 8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailProviderManager.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8E3FE65EE63CBA65592863C2 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; - 8F7D1C4699CE71F08DC5B016 /* FullscreenLoadingActivityPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenLoadingActivityPresenter.swift; sourceTree = ""; }; 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = ""; }; 938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = ""; }; 93B21E72926FACB13A186689 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ml; path = ml.lproj/Localizable.stringsdict; sourceTree = ""; }; 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; - 9525FEE63EBF02C97161BF7D /* ActivityRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityRequest.swift; sourceTree = ""; }; 956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; 95CC95CD75B688E946438165 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; @@ -355,14 +373,19 @@ ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; AE225C66978648AA4AF37B45 /* te */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = te; path = te.lproj/Localizable.strings; sourceTree = ""; }; AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; + AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; + AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedToastView.swift; sourceTree = ""; }; B08CBE1E670690ECF11C2C6A /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = eu; path = eu.lproj/Localizable.stringsdict; sourceTree = ""; }; B0C5E5931A668B18D8C09028 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/Localizable.strings; sourceTree = ""; }; B1183B55FF4B01022DA721CB /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; B12969CEC0051BC750DA5068 /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = ""; }; + B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorDismissal.swift; sourceTree = ""; }; B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; + B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = ""; }; + B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenterSpy.swift; sourceTree = ""; }; B6BDAC8895AB2B77B47703AE /* MockRoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummary.swift; sourceTree = ""; }; B7E035C6AC137C9392D98814 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Localizable.strings; sourceTree = ""; }; B80D1901BA0B095E27793EDE /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; @@ -381,6 +404,7 @@ C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; + C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueue.swift; sourceTree = ""; }; C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C9F395A2E917115C7AAF7F34 /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; CAAE4A709C0A2144C103AA0F /* ang */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ang; path = ang.lproj/Localizable.strings; sourceTree = ""; }; @@ -402,10 +426,9 @@ D67CBAFA48ED0B6FCE74F88F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D77DD2DA5DC8654F2A80FF1D /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; - D867D38F60E364DA470C131D /* RectangleToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleToastView.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; - DC54146B646F161762B54BBF /* ActivityPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityPresentable.swift; sourceTree = ""; }; - E09C9DFFE9A897E439D770C5 /* ToastActivityPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastActivityPresenter.swift; sourceTree = ""; }; + DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = ""; }; + E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenter.swift; sourceTree = ""; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; @@ -419,7 +442,6 @@ E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED659E775C4FCEBC977C6A08 /* ActivityCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCenter.swift; sourceTree = ""; }; EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; @@ -593,15 +615,6 @@ path = View; sourceTree = ""; }; - 52C7F31749CD188294292122 /* Toasts */ = { - isa = PBXGroup; - children = ( - D867D38F60E364DA470C131D /* RectangleToastView.swift */, - 5B4238D6EB22C0F9D7A2EEC9 /* RoundedToastView.swift */, - ); - path = Toasts; - sourceTree = ""; - }; 5958CAF6E56422496E0063AF /* LoginScreen */ = { isa = PBXGroup; children = ( @@ -658,6 +671,7 @@ C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, + AF552BB969DC98A4BB8CF8D5 /* UserIndicators */, ); path = Sources; sourceTree = ""; @@ -709,21 +723,6 @@ path = Media; sourceTree = ""; }; - 8EDC2B4DFB37263DDCE0E8D1 /* Activity */ = { - isa = PBXGroup; - children = ( - 43B33C10BC91ABFF09605DB6 /* Activity.swift */, - ED659E775C4FCEBC977C6A08 /* ActivityCenter.swift */, - 34FB01A9AC30E2FA59678BC5 /* ActivityDismissal.swift */, - DC54146B646F161762B54BBF /* ActivityPresentable.swift */, - 9525FEE63EBF02C97161BF7D /* ActivityRequest.swift */, - 6B3947F320B9F5748C736AD3 /* LabelledActivityIndicatorView.swift */, - A04A0AFBCA9BCC4B44334981 /* ActivityPresenters */, - 52C7F31749CD188294292122 /* Toasts */, - ); - path = Activity; - sourceTree = ""; - }; 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */ = { isa = PBXGroup; children = ( @@ -783,15 +782,6 @@ path = WeakDictionary; sourceTree = ""; }; - A04A0AFBCA9BCC4B44334981 /* ActivityPresenters */ = { - isa = PBXGroup; - children = ( - 8F7D1C4699CE71F08DC5B016 /* FullscreenLoadingActivityPresenter.swift */, - E09C9DFFE9A897E439D770C5 /* ToastActivityPresenter.swift */, - ); - path = ActivityPresenters; - sourceTree = ""; - }; A0C06C0F6A8621B22BFAEB56 /* Localizations */ = { isa = PBXGroup; children = ( @@ -823,6 +813,16 @@ path = Authentication; sourceTree = ""; }; + AF552BB969DC98A4BB8CF8D5 /* UserIndicators */ = { + isa = PBXGroup; + children = ( + B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */, + 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */, + 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */, + ); + path = UserIndicators; + sourceTree = ""; + }; B53CA9BECD3F97805E1432D0 /* HomeScreen */ = { isa = PBXGroup; children = ( @@ -859,10 +859,10 @@ 95CC95CD75B688E946438165 /* Coordinator.swift */, F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */, 64839516BD56D1C81D84C5E0 /* MXLog.swift */, - 8EDC2B4DFB37263DDCE0E8D1 /* Activity */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, FE50232944F9E67ADD7A2D21 /* Routers */, 052CC920F473C10B509F9FC1 /* SwiftUI */, + F8474EB69289112888B65518 /* UserIndicators */, 9D1AFFAEB84C76D1B4FFBB3A /* WeakDictionary */, ); path = Other; @@ -903,6 +903,31 @@ path = Sources; sourceTree = ""; }; + F8474EB69289112888B65518 /* UserIndicators */ = { + isa = PBXGroup; + children = ( + 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */, + 85C2318DF4C0E601EEE31F84 /* ActivityIndicatorPresenterType.swift */, + AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */, + B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */, + 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */, + 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */, + 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */, + AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */, + 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */, + 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */, + DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */, + B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */, + 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */, + E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */, + C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */, + 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */, + 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */, + 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */, + ); + path = UserIndicators; + sourceTree = ""; + }; FCDF06BDB123505F0334B4F9 /* Timeline */ = { isa = PBXGroup; children = ( @@ -1115,6 +1140,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */, B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */, 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */, AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */, @@ -1195,6 +1221,9 @@ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, + 1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */, + 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */, + BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1202,11 +1231,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D735AF72894A273F53D941B8 /* Activity.swift in Sources */, - B4EF927F0C14F2F3F8D63DE8 /* ActivityCenter.swift in Sources */, - 417E2190C02B96F498592559 /* ActivityDismissal.swift in Sources */, - 3C732330B5A3D0A64142B280 /* ActivityPresentable.swift in Sources */, - 7C72B61B30B41D76953237A8 /* ActivityRequest.swift in Sources */, + D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */, + 4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */, + FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */, A636D4900E0D98ED91536482 /* AppCoordinator.swift in Sources */, 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */, 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */, @@ -1230,7 +1257,7 @@ 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */, F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */, A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */, - 3C7009EB92E77136FF7BD9D7 /* FullscreenLoadingActivityPresenter.swift in Sources */, + 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */, @@ -1241,7 +1268,7 @@ D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, 2D8A687149E46B8C8B989561 /* KeychainController.swift in Sources */, 277D2531C70F207A2F9F5906 /* KeychainControllerProtocol.swift in Sources */, - 539F448EC0250880703775DE /* LabelledActivityIndicatorView.swift in Sources */, + 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */, D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */, A941EAD7F407F2ED6DA54A31 /* LoginScreen.swift in Sources */, 306CC09DF101E7E9CDE79AA5 /* LoginScreenCoordinator.swift in Sources */, @@ -1272,7 +1299,7 @@ 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */, BF35062D06888FA80BD139FF /* Presentable.swift in Sources */, - 51910CB919287139F2DF6261 /* RectangleToastView.swift in Sources */, + 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */, FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */, D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */, 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */, @@ -1294,7 +1321,7 @@ CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */, 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */, 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */, - 7684092532FBC71B73916441 /* RoundedToastView.swift in Sources */, + 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */, 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */, 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */, FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */, @@ -1307,10 +1334,19 @@ 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */, 4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */, 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */, - 15FEC447B7FEA62F418732AC /* ToastActivityPresenter.swift in Sources */, + 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */, + 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */, 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */, 03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */, 17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */, + 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */, + 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */, + 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */, + 7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */, + 10866439ABA58CCDB5D1459D /* UserIndicatorQueue.swift in Sources */, + 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */, + C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */, + 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */, DD4ADDB73E0935B74D2D18D6 /* UserSession.swift in Sources */, 01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */, 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */, @@ -1528,7 +1564,7 @@ SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "ElementX/SupportingFiles/ElementX-Bridging-Header.h"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1552,7 +1588,7 @@ SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "ElementX/SupportingFiles/ElementX-Bridging-Header.h"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 490a54690..3bcadd060 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -19,9 +19,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private let authenticationCoordinator: AuthenticationCoordinator! private let memberDetailProviderManager: MemberDetailProviderManager - - private var loadingActivity: Activity? - private var errorActivity: Activity? + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + private var errorIndicator: UserIndicator? var childCoordinators: [Coordinator] = [] @@ -35,6 +36,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { memberDetailProviderManager = MemberDetailProviderManager() + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: mainNavigationController) + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { fatalError("Should have a valid bundle identifier at this point") } @@ -143,30 +146,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } private func showLoadingIndicator() { - let presenter = FullscreenLoadingActivityPresenter(label: "Loading", - on: mainNavigationController) - - let request = ActivityRequest( - presenter: presenter, - dismissal: .manual - ) - - loadingActivity = ActivityCenter.shared.add(request) + loadingIndicator = indicatorPresenter.present(.loading(label: "Loading", isInteractionBlocking: true)) } private func hideLoadingIndicator() { - loadingActivity = nil + loadingIndicator = nil } private func showLoginErrorToast() { - let presenter = ToastActivityPresenter(viewState: .init(style: .success, label: "Failed logging in"), - navigationController: mainNavigationController) - - let request = ActivityRequest( - presenter: presenter, - dismissal: .timeout(3.0) - ) - - errorActivity = ActivityCenter.shared.add(request) + errorIndicator = indicatorPresenter.present(.success(label: "Failed logging in")) } } diff --git a/ElementX/Sources/Other/Activity/ActivityPresenters/FullscreenLoadingActivityPresenter.swift b/ElementX/Sources/Other/Activity/ActivityPresenters/FullscreenLoadingActivityPresenter.swift deleted file mode 100644 index 8ae1fb3df..000000000 --- a/ElementX/Sources/Other/Activity/ActivityPresenters/FullscreenLoadingActivityPresenter.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import UIKit - -/// An `ActivityPresenter` responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls. -/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes. -class FullscreenLoadingActivityPresenter: ActivityPresentable { - private let label: String - private weak var viewController: UIViewController? - private weak var view: UIView? - - init(label: String, on viewController: UIViewController) { - self.label = label - self.viewController = viewController - } - - func present() { - let view = LabelledActivityIndicatorView(text: label) - self.view = view - - guard let window = viewController?.view.window else { - return - } - - view.translatesAutoresizingMaskIntoConstraints = false - window.addSubview(view) - NSLayoutConstraint.activate([ - view.topAnchor.constraint(equalTo: window.topAnchor), - view.bottomAnchor.constraint(equalTo: window.bottomAnchor), - view.leadingAnchor.constraint(equalTo: window.leadingAnchor), - view.trailingAnchor.constraint(equalTo: window.trailingAnchor) - ]) - - view.alpha = 0 - CATransaction.commit() - UIView.animate(withDuration: 0.2) { - view.alpha = 1 - } - } - - func dismiss() { - guard let view = view, view.superview != nil else { - return - } - - UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { - view.alpha = 0 - } completion: { _ in - view.removeFromSuperview() - } - } -} diff --git a/ElementX/Sources/Other/Activity/ActivityPresenters/ToastActivityPresenter.swift b/ElementX/Sources/Other/Activity/ActivityPresenters/ToastActivityPresenter.swift deleted file mode 100644 index df77fb314..000000000 --- a/ElementX/Sources/Other/Activity/ActivityPresenters/ToastActivityPresenter.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import UIKit - -/// An `ActivityPresenter` responsible for showing / hiding a toast view for activity indicators or success messages. -/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes. -class ToastActivityPresenter: ActivityPresentable { - private let viewState: RoundedToastView.ViewState - private weak var navigationController: UINavigationController? - private weak var view: UIView? - - init(viewState: RoundedToastView.ViewState, navigationController: UINavigationController) { - self.viewState = viewState - self.navigationController = navigationController - } - - func present() { - guard let navigationController = navigationController, - let window = navigationController.view.window else { - return - } - - let view = RoundedToastView(viewState: viewState) - self.view = view - - view.translatesAutoresizingMaskIntoConstraints = false - window.addSubview(view) - NSLayoutConstraint.activate([ - view.centerXAnchor.constraint(equalTo: navigationController.navigationBar.centerXAnchor), - view.topAnchor.constraint(equalTo: navigationController.navigationBar.bottomAnchor) - ]) - - view.alpha = 0 - CATransaction.flush() - view.transform = .init(translationX: 0, y: 5) - - UIView.animate(withDuration: 0.2) { - view.alpha = 1 - view.transform = .identity - } - } - - func dismiss() { - guard let view = view, view.superview != nil else { - return - } - - UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { - view.alpha = 0 - view.transform = .init(translationX: 0, y: -5) - } completion: { _ in - view.removeFromSuperview() - } - } -} diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift new file mode 100755 index 000000000..de5c1876f --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift @@ -0,0 +1,148 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +/// Used to present activity indicator on a view +final class ActivityIndicatorPresenter: ActivityIndicatorPresenterType { + + // MARK: - Constants + + private enum Constants { + static let animationDuration: TimeInterval = 0.3 + static let backgroundOverlayColor = UIColor.clear + static let backgroundOverlayAlpha: CGFloat = 1.0 + } + + // MARK: - Properties + + private weak var backgroundOverlayView: UIView? + private weak var activityIndicatorView: ActivityIndicatorView? + private weak var presentingView: UIView? + + var isPresenting: Bool { + return self.activityIndicatorView != nil + } + + // MARK: - Public + + func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)? = nil) { + if self.presentingView != nil { + if let completion = completion { + completion() + } + return + } + + self.presentingView = view + + view.isUserInteractionEnabled = false + + let backgroundOverlayView = self.createBackgroundOverlayView(with: view.frame) + + let activityIndicatorView = ActivityIndicatorView() + + // Add activityIndicatorView on backgroundOverlayView centered + backgroundOverlayView.addSubview(activityIndicatorView) + activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + activityIndicatorView.centerXAnchor.constraint(equalTo: backgroundOverlayView.centerXAnchor).isActive = true + activityIndicatorView.centerYAnchor.constraint(equalTo: backgroundOverlayView.centerYAnchor).isActive = true + + activityIndicatorView.startAnimating() + + backgroundOverlayView.alpha = 0 + backgroundOverlayView.isHidden = false + + view.vc_addSubViewMatchingParent(backgroundOverlayView) + + self.backgroundOverlayView = backgroundOverlayView + self.activityIndicatorView = activityIndicatorView + + let animationInstructions = { + backgroundOverlayView.alpha = Constants.backgroundOverlayAlpha + } + + if animated { + UIView.animate(withDuration: Constants.animationDuration, animations: { + animationInstructions() + }, completion: { _ in + completion?() + }) + } else { + animationInstructions() + completion?() + } + } + + func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)? = nil) { + guard let presentingView = self.presentingView, + let backgroundOverlayView = self.backgroundOverlayView, + let activityIndicatorView = self.activityIndicatorView else { + return + } + + presentingView.isUserInteractionEnabled = true + self.presentingView = nil + + let animationInstructions = { + activityIndicatorView.alpha = 0 + } + + let animationCompletionInstructions = { + activityIndicatorView.stopAnimating() + backgroundOverlayView.isHidden = true + backgroundOverlayView.removeFromSuperview() + } + + if animated { + UIView.animate(withDuration: Constants.animationDuration, animations: { + animationInstructions() + }, completion: { _ in + animationCompletionInstructions() + }) + } else { + animationInstructions() + animationCompletionInstructions() + } + } + + // MARK: - Private + + private func createBackgroundOverlayView(with frame: CGRect = CGRect.zero) -> UIView { + let backgroundOverlayView = UIView(frame: frame) + backgroundOverlayView.backgroundColor = Constants.backgroundOverlayColor + backgroundOverlayView.alpha = Constants.backgroundOverlayAlpha + return backgroundOverlayView + } +} + +private extension UIView { + + /// Add a subview matching parent view using autolayout + @objc func vc_addSubViewMatchingParent(_ subView: UIView) { + self.addSubview(subView) + subView.translatesAutoresizingMaskIntoConstraints = false + let views = ["view": subView] + ["H:|[view]|", "V:|[view]|"].forEach { vfl in + let constraints = NSLayoutConstraint.constraints(withVisualFormat: vfl, + options: [], + metrics: nil, + views: views) + constraints.forEach { $0.isActive = true } + } + } +} diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift new file mode 100644 index 000000000..6b90389f5 --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift @@ -0,0 +1,34 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +/// Protocol used to present activity indicator on a view +protocol ActivityIndicatorPresenterType { + func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) + func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) +} + +// `ActivityIndicatorPresenterType` default implementation +extension ActivityIndicatorPresenterType { + func presentActivityIndicator(on view: UIView, animated: Bool) { + self.presentActivityIndicator(on: view, animated: animated, completion: nil) + } + + func removeCurrentActivityIndicator(animated: Bool) { + self.removeCurrentActivityIndicator(animated: animated, completion: nil) + } +} diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift new file mode 100755 index 000000000..79f91dae4 --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift @@ -0,0 +1,112 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +final class ActivityIndicatorView: UIView { + + // MARK: - Constants + + private enum Constants { + static let cornerRadius: CGFloat = 5.0 + static let activityIndicatorMargin = CGSize(width: 30.0, height: 30.0) + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var activityIndicatorView: UIActivityIndicatorView! + @IBOutlet private weak var activityIndicatorBackgroundView: UIView! + + // MARK: Public + + var color: UIColor? { + get { + return activityIndicatorView.color + } + set { + activityIndicatorView.color = newValue + } + } + + // MARK: - Setup + + private func commonInit() { + self.activityIndicatorBackgroundView.layer.masksToBounds = true + } + + convenience init() { + self.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadNibContent() + self.commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadNibContent() + self.commonInit() + } + + // MARK: - Overrides + + override var intrinsicContentSize: CGSize { + return CGSize(width: self.activityIndicatorView.intrinsicContentSize.width + Constants.activityIndicatorMargin.width, + height: self.activityIndicatorView.intrinsicContentSize.height + Constants.activityIndicatorMargin.height) + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.activityIndicatorBackgroundView.layer.cornerRadius = Constants.cornerRadius + } + + // MARK: - Public + + func startAnimating() { + self.activityIndicatorView.startAnimating() + } + + func stopAnimating() { + self.activityIndicatorView.stopAnimating() + } +} + +private extension UIView { + static var nib: UINib { + return UINib(nibName: String(describing: self), bundle: Bundle(for: self)) + } + + func loadNibContent() { + let layoutAttributes: [NSLayoutConstraint.Attribute] = [.top, .leading, .bottom, .trailing] + for case let view as UIView in type(of: self).nib.instantiate(withOwner: self, options: nil) { + view.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(view) + NSLayoutConstraint.activate(layoutAttributes.map { attribute in + NSLayoutConstraint( + item: view, attribute: attribute, + relatedBy: .equal, + toItem: self, attribute: attribute, + multiplier: 1, constant: 0.0 + ) + }) + } + } +} diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib new file mode 100755 index 000000000..874a4e665 --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift b/ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift new file mode 100644 index 000000000..ca4404680 --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift @@ -0,0 +1,75 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// A presenter responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls. +/// It is managed by a `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes. +class FullscreenLoadingViewPresenter: UserIndicatorViewPresentable { + private let label: String + private let presentationContext: UserIndicatorPresentationContext + private weak var view: UIView? + private var animator: UIViewPropertyAnimator? + + init(label: String, presentationContext: UserIndicatorPresentationContext) { + self.label = label + self.presentationContext = presentationContext + } + + func present() { + // Find the current top navigation controller + var presentingController: UIViewController? = presentationContext.indicatorPresentingViewController + while presentingController?.navigationController != nil { + presentingController = presentingController?.navigationController + } + guard let presentingController = presentingController else { + return + } + + let view = LabelledActivityIndicatorView(text: label) + self.view = view + + view.translatesAutoresizingMaskIntoConstraints = false + presentingController.view.addSubview(view) + NSLayoutConstraint.activate([ + view.topAnchor.constraint(equalTo: presentingController.view.topAnchor), + view.bottomAnchor.constraint(equalTo: presentingController.view.bottomAnchor), + view.leadingAnchor.constraint(equalTo: presentingController.view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: presentingController.view.trailingAnchor) + ]) + + view.alpha = 0 + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) { + view.alpha = 1 + } + animator?.startAnimation() + } + + func dismiss() { + guard let view = view, view.superview != nil else { + return + } + + animator?.stopAnimation(true) + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) { + view.alpha = 0 + } + animator?.addCompletion { _ in + view.removeFromSuperview() + } + animator?.startAnimation() + } +} diff --git a/ElementX/Sources/Other/Activity/LabelledActivityIndicatorView.swift b/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift similarity index 100% rename from ElementX/Sources/Other/Activity/LabelledActivityIndicatorView.swift rename to ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift diff --git a/ElementX/Sources/Other/Activity/Toasts/RectangleToastView.swift b/ElementX/Sources/Other/UserIndicators/RectangleToastView.swift similarity index 100% rename from ElementX/Sources/Other/Activity/Toasts/RectangleToastView.swift rename to ElementX/Sources/Other/UserIndicators/RectangleToastView.swift diff --git a/ElementX/Sources/Other/Activity/Toasts/RoundedToastView.swift b/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift similarity index 78% rename from ElementX/Sources/Other/Activity/Toasts/RoundedToastView.swift rename to ElementX/Sources/Other/UserIndicators/RoundedToastView.swift index 23fdb9eb5..8488e7ff9 100644 --- a/ElementX/Sources/Other/Activity/Toasts/RoundedToastView.swift +++ b/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift @@ -14,17 +14,21 @@ // limitations under the License. // -import Foundation import UIKit class RoundedToastView: UIView { + private struct ShadowStyle { + let offset: CGSize + let radius: CGFloat + let opacity: Float + } + private struct Constants { static let padding = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) static let activityIndicatorScale = CGFloat(0.75) static let imageViewSize = CGFloat(15) - static let shadowOffset = CGSize(width: 0, height: 4) - static let shadowRadius = CGFloat(12) - static let shadowOpacity = Float(0.1) + static let lightShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 12, opacity: 0.1) + static let darkShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 4, opacity: 0.2) } private lazy var activityIndicator: UIActivityIndicatorView = { @@ -34,7 +38,7 @@ class RoundedToastView: UIView { return indicator }() - private lazy var imagView: UIImageView = { + private lazy var imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.translatesAutoresizingMaskIntoConstraints = false @@ -57,7 +61,7 @@ class RoundedToastView: UIView { return UILabel() }() - init(viewState: ViewState) { + init(viewState: ToastViewState) { super.init(frame: .zero) setup(viewState: viewState) } @@ -66,11 +70,10 @@ class RoundedToastView: UIView { fatalError("init(coder:) has not been implemented") } - private func setup(viewState: ViewState) { + private func setup(viewState: ToastViewState) { backgroundColor = .gray.withAlphaComponent(0.75) - setupLayer() setupStackView() stackView.addArrangedSubview(toastView(for: viewState.style)) stackView.addArrangedSubview(label) @@ -88,36 +91,18 @@ class RoundedToastView: UIView { ]) } - private func setupLayer() { - layer.shadowColor = UIColor.black.cgColor - layer.shadowOffset = Constants.shadowOffset - layer.shadowRadius = Constants.shadowRadius - layer.shadowOpacity = Constants.shadowOpacity - } - override func layoutSubviews() { super.layoutSubviews() layer.cornerRadius = layer.frame.height / 2 } - private func toastView(for style: Style) -> UIView { + private func toastView(for style: ToastViewState.Style) -> UIView { switch style { case .loading: return activityIndicator case .success: - imagView.image = UIImage(systemName: "checkmark.circle") - return imagView + imageView.image = UIImage(systemName: "checkmark.circle") + return imageView } } } - -extension RoundedToastView { - enum Style { - case loading - case success - } - struct ViewState { - let style: Style - let label: String - } -} diff --git a/ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift b/ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift new file mode 100644 index 000000000..5785ed6ac --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift @@ -0,0 +1,93 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// A presenter responsible for showing / hiding a toast view for loading spinners or success messages. +/// It is managed by an `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes. +class ToastViewPresenter: UserIndicatorViewPresentable { + struct Constants { + static let navigationBarPatting = CGFloat(10) + } + + private let viewState: ToastViewState + private let presentationContext: UserIndicatorPresentationContext + private weak var view: UIView? + private var animator: UIViewPropertyAnimator? + + init(viewState: ToastViewState, presentationContext: UserIndicatorPresentationContext) { + self.viewState = viewState + self.presentationContext = presentationContext + } + + func present() { + guard let viewController = presentationContext.indicatorPresentingViewController else { + return + } + + let view = RoundedToastView(viewState: viewState) + self.view = view + + view.translatesAutoresizingMaskIntoConstraints = false + if let navigation = viewController.topNavigationController { + navigation.view.addSubview(view) + NSLayoutConstraint.activate([ + view.centerXAnchor.constraint(equalTo: navigation.view.centerXAnchor), + view.topAnchor.constraint(equalTo: navigation.navigationBar.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.navigationBarPatting) + ]) + } else { + viewController.view.addSubview(view) + NSLayoutConstraint.activate([ + view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor), + view.topAnchor.constraint(equalTo: viewController.view.topAnchor) + ]) + } + + view.alpha = 0 + view.transform = .init(translationX: 0, y: 5) + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) { + view.alpha = 1 + view.transform = .identity + } + animator?.startAnimation() + } + + func dismiss() { + guard let view = view, view.superview != nil else { + return + } + + animator?.stopAnimation(true) + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) { + view.alpha = 0 + view.transform = .init(translationX: 0, y: -5) + } + animator?.addCompletion { _ in + view.removeFromSuperview() + } + animator?.startAnimation() + } +} + +private extension UIViewController { + var topNavigationController: UINavigationController? { + var controller: UINavigationController? = self as? UINavigationController ?? navigationController + while controller?.navigationController != nil { + controller = controller?.navigationController + } + return controller + } +} diff --git a/ElementX/Sources/Other/UserIndicators/ToastViewState.swift b/ElementX/Sources/Other/UserIndicators/ToastViewState.swift new file mode 100644 index 000000000..e241d4645 --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/ToastViewState.swift @@ -0,0 +1,27 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct ToastViewState { + enum Style { + case loading + case success + } + + let style: Style + let label: String +} diff --git a/ElementX/Sources/Other/Activity/Activity.swift b/ElementX/Sources/Other/UserIndicators/UserIndicator.swift similarity index 63% rename from ElementX/Sources/Other/Activity/Activity.swift rename to ElementX/Sources/Other/UserIndicators/UserIndicator.swift index 5c8eaac0b..d2ad5ab2d 100644 --- a/ElementX/Sources/Other/Activity/Activity.swift +++ b/ElementX/Sources/Other/UserIndicators/UserIndicator.swift @@ -17,27 +17,27 @@ import Foundation import UIKit -/// An `Activity` represents the state of a temporary visual indicator, such as activity indicator, success notification or an error message. It does not directly manage the UI, instead it delegates to a `presenter` +/// A `UserIndicator` represents the state of a temporary visual indicator, such as loading spinner, success notification or an error message. It does not directly manage the UI, instead it delegates to a `presenter` /// whenever the UI should be shown or hidden. /// -/// More than one `Activity` may be requested by the system at the same time (e.g. global syncing vs local refresh), -/// and the `ActivityCenter` will ensure that only one activity is shown at a given time, putting the other in a pending queue. +/// More than one `UserIndicator` may be requested by the system at the same time (e.g. global syncing vs local refresh), +/// and the `UserIndicatorQueue` will ensure that only one indicator is shown at a given time, putting the other in a pending queue. /// -/// A client that requests an activity can specify a default timeout after which the activity is dismissed, or it has to be manually +/// A client that requests an indicator can specify a default timeout after which the indicator is dismissed, or it has to be manually /// responsible for dismissing it via `cancel` method, or by deallocating itself. -public class Activity { +public class UserIndicator { public enum State { case pending case executing case completed } - private let request: ActivityRequest + private let request: UserIndicatorRequest private let completion: () -> Void public private(set) var state: State - public init(request: ActivityRequest, completion: @escaping () -> Void) { + public init(request: UserIndicatorRequest, completion: @escaping () -> Void) { self.request = request self.completion = completion @@ -66,10 +66,11 @@ public class Activity { } } - /// Cancel the activity, triggering any dismissal action / animation + /// Cancel the indicator, triggering any dismissal action / animation /// - /// Note: clients can call this method directly, if they have access to the `Activity`. - /// Once cancelled, `ActivityCenter` will automatically start the next `Activity` in the queue. + /// Note: clients can call this method directly, if they have access to the `UserIndicator`. Alternatively + /// deallocating the `UserIndicator` will call `cancel` automatically. + /// Once cancelled, `UserIndicatorQueue` will automatically start the next `UserIndicator` in the queue. public func cancel() { complete() } @@ -87,13 +88,13 @@ public class Activity { } } -public extension Activity { - func store(in collection: inout C) where C: RangeReplaceableCollection, C.Element == Activity { +public extension UserIndicator { + func store(in collection: inout C) where C: RangeReplaceableCollection, C.Element == UserIndicator { collection.append(self) } } -public extension Collection where Element == Activity { +public extension Collection where Element == UserIndicator { func cancelAll() { forEach { $0.cancel() diff --git a/ElementX/Sources/Other/Activity/ActivityDismissal.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorDismissal.swift similarity index 69% rename from ElementX/Sources/Other/Activity/ActivityDismissal.swift rename to ElementX/Sources/Other/UserIndicators/UserIndicatorDismissal.swift index 69405c579..874a4f1ed 100644 --- a/ElementX/Sources/Other/Activity/ActivityDismissal.swift +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorDismissal.swift @@ -16,10 +16,10 @@ import Foundation -/// Different ways in which an `Activity` can be dismissed -public enum ActivityDismissal { - /// The `Activity` will not manage the dismissal, but will expect the calling client to do so manually +/// Different ways in which a `UserIndicator` can be dismissed +public enum UserIndicatorDismissal { + /// The `UserIndicator` will not manage the dismissal, but will expect the calling client to do so manually case manual - /// The `Activity` will be automatically dismissed after `TimeInterval` + /// The `UserIndicator` will be automatically dismissed after `TimeInterval` case timeout(TimeInterval) } diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift new file mode 100644 index 000000000..94656ad6b --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift @@ -0,0 +1,42 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/// The presentation context is used by `UserIndicatorViewPresentable`s to display content +/// on the screen and it serves two primary purposes: +/// +/// - abstraction on top of UIKit (passing context instead of view controllers) +/// - immutable context passed at init with variable presenting view controller +/// (e.g. depending on collapsed / uncollapsed iPad presentation that changes +/// at runtime) +public protocol UserIndicatorPresentationContext { + var indicatorPresentingViewController: UIViewController? { get } +} + +/// A simple implementation of `UserIndicatorPresentationContext` that uses a weak reference +/// to the passed-in view controller as the presentation context. +public class StaticUserIndicatorPresentationContext: UserIndicatorPresentationContext { + // The presenting view controller will be the parent of the user indicator, + // and the indicator holds a strong reference to the context, so the view controller + // must be decleared `weak` to avoid a retain cycle + public private(set) weak var indicatorPresentingViewController: UIViewController? + + public init(viewController: UIViewController) { + self.indicatorPresentingViewController = viewController + } +} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift new file mode 100644 index 000000000..dc5d39d6c --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift @@ -0,0 +1,116 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// A set of user interactors commonly used across the app +enum UserIndicatorType { + case loading(label: String, isInteractionBlocking: Bool) + case success(label: String) +} + +/// A presenter which can handle `UserIndicatorType` by creating the underlying `UserIndicator` +/// and adding it to its `UserIndicatorQueue` +protocol UserIndicatorTypePresenterProtocol { + /// Present a new type of user indicator, such as loading spinner or success message. + /// + /// The presenter will internally convert the type into a `UserIndicator` and add it to its internal queue + /// of other indicators. + /// + /// If the queue is empty, the indicator will be displayed immediately, otherwise it will be pending + /// until the previously added indicators have completed / been cancelled. + /// + /// To remove an indicator, `cancel` or deallocate the returned `UserIndicator` + func present(_ type: UserIndicatorType) -> UserIndicator + + /// The queue of user indicators owned by the presenter + /// + /// Clients can access the queue to add custom `UserIndicatorRequest`s + /// above and beyond those defined by `UserIndicatorType` + var queue: UserIndicatorQueue { get } +} + +class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol { + private let presentationContext: UserIndicatorPresentationContext + let queue: UserIndicatorQueue + + init(presentationContext: UserIndicatorPresentationContext) { + self.presentationContext = presentationContext + self.queue = UserIndicatorQueue() + } + + convenience init(presentingViewController: UIViewController) { + let context = StaticUserIndicatorPresentationContext(viewController: presentingViewController) + self.init(presentationContext: context) + } + + func present(_ type: UserIndicatorType) -> UserIndicator { + let request = userIndicatorRequest(for: type) + return queue.add(request) + } + + private func userIndicatorRequest(for type: UserIndicatorType) -> UserIndicatorRequest { + switch type { + case .loading(let label, let isInteractionBlocking): + if isInteractionBlocking { + return fullScreenLoadingRequest(label: label) + } else { + return loadingRequest(label: label) + } + case .success(let label): + return successRequest(label: label) + } + } + + private func loadingRequest(label: String) -> UserIndicatorRequest { + let presenter = ToastViewPresenter( + viewState: .init( + style: .loading, + label: label + ), + presentationContext: presentationContext + ) + return UserIndicatorRequest( + presenter: presenter, + dismissal: .manual + ) + } + + private func fullScreenLoadingRequest(label: String) -> UserIndicatorRequest { + let presenter = FullscreenLoadingViewPresenter( + label: label, + presentationContext: presentationContext + ) + return UserIndicatorRequest( + presenter: presenter, + dismissal: .manual + ) + } + + private func successRequest(label: String) -> UserIndicatorRequest { + let presenter = ToastViewPresenter( + viewState: .init( + style: .success, + label: label + ), + presentationContext: presentationContext + ) + return UserIndicatorRequest( + presenter: presenter, + dismissal: .timeout(1.5) + ) + } +} diff --git a/ElementX/Sources/Other/Activity/ActivityCenter.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorQueue.swift similarity index 52% rename from ElementX/Sources/Other/Activity/ActivityCenter.swift rename to ElementX/Sources/Other/UserIndicators/UserIndicatorQueue.swift index 9d9e0c704..a2ba49fcf 100644 --- a/ElementX/Sources/Other/Activity/ActivityCenter.swift +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorQueue.swift @@ -16,11 +16,11 @@ import Foundation -/// A shared activity center with a single FIFO queue which will ensure only one activity is shown at a given time. +/// A FIFO queue which will ensure only one user indicator is shown at a given time. /// -/// `ActivityCenter` offers a `shared` center that can be used by any clients, but clients are also allowed -/// to create local `ActivityCenter` if the context requres multiple simultaneous activities. -public class ActivityCenter { +/// `UserIndicatorQueue` offers a `shared` queue that can be used by any clients app-wide, but clients are also allowed +/// to create local `UserIndicatorQueue` if the context requres multiple simultaneous indicators. +public class UserIndicatorQueue { private class Weak { weak var element: T? init(_ element: T) { @@ -28,27 +28,30 @@ public class ActivityCenter { } } - public static let shared = ActivityCenter() - private var queue = [Weak]() + private var queue: [Weak] - /// Add a new activity to the queue by providing a request. + public init() { + queue = [] + } + + /// Add a new indicator to the queue by providing a request. /// - /// The queue will start the activity right away, if there are no currently running activities, - /// otherwise the activity will be put on hold. - public func add(_ request: ActivityRequest) -> Activity { - let activity = Activity(request: request) { [weak self] in + /// The queue will start the indicator right away, if there are no currently running indicators, + /// otherwise the indicator will be put on hold. + public func add(_ request: UserIndicatorRequest) -> UserIndicator { + let indicator = UserIndicator(request: request) { [weak self] in self?.startNextIfIdle() } - queue.append(Weak(activity)) + queue.append(Weak(indicator)) startNextIfIdle() - return activity + return indicator } private func startNextIfIdle() { cleanup() - if let activity = queue.first?.element, activity.state == .pending { - activity.start() + if let indicator = queue.first?.element, indicator.state == .pending { + indicator.start() } } diff --git a/ElementX/Sources/Other/Activity/ActivityRequest.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorRequest.swift similarity index 65% rename from ElementX/Sources/Other/Activity/ActivityRequest.swift rename to ElementX/Sources/Other/UserIndicators/UserIndicatorRequest.swift index f009fc211..97611eeea 100644 --- a/ElementX/Sources/Other/Activity/ActivityRequest.swift +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorRequest.swift @@ -16,12 +16,12 @@ import Foundation -/// A request used to create an underlying `Activity`, allowing clients to only specify the visual aspects of an activity. -public struct ActivityRequest { - internal let presenter: ActivityPresentable - internal let dismissal: ActivityDismissal +/// A request used to create an underlying `UserIndicator`, allowing clients to only specify the visual aspects of an indicator. +public struct UserIndicatorRequest { + internal let presenter: UserIndicatorViewPresentable + internal let dismissal: UserIndicatorDismissal - public init(presenter: ActivityPresentable, dismissal: ActivityDismissal) { + public init(presenter: UserIndicatorViewPresentable, dismissal: UserIndicatorDismissal) { self.presenter = presenter self.dismissal = dismissal } diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift new file mode 100644 index 000000000..c7734b637 --- /dev/null +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift @@ -0,0 +1,63 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +typealias UserIndicatorCancel = () -> Void + +/// An abstraction on top of `UserIndicatorTypePresenterProtocol` which manages and stores the individual user indicators. +/// When used to present an indicator the `UserIndicatorStore` will instead returns a simple callback function to the clients +/// letting them cancel the indicators without worrying about memory. +@objc final class UserIndicatorStore: NSObject { + private let presenter: UserIndicatorTypePresenterProtocol + private var indicators: [UserIndicator] + + init(presenter: UserIndicatorTypePresenterProtocol) { + self.presenter = presenter + self.indicators = [] + } + + /// Present a new type of user indicator, such as loading spinner or success message. + /// To remove an indicator, call the returned `UserIndicatorCancel` function + func present(type: UserIndicatorType) -> UserIndicatorCancel { + let indicator = presenter.present(type) + indicators.append(indicator) + return { + indicator.cancel() + } + } + + /// Present a loading indicator. + /// To remove the indicator call the returned `UserIndicatorCancel` function + /// + /// Note: This is a convenience function callable by objective-c code + @objc func presentLoading(label: String, isInteractionBlocking: Bool) -> UserIndicatorCancel { + present( + type: .loading( + label: label, + isInteractionBlocking: isInteractionBlocking + ) + ) + } + + /// Present a success message that will be automatically dismissed after a few seconds. + /// + /// Note: This is a convenience function callable by objective-c code + @objc func presentSuccess(label: String) { + let indicator = presenter.present(.success(label: label)) + indicators.append(indicator) + } +} diff --git a/ElementX/Sources/Other/Activity/ActivityPresentable.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorViewPresentable.swift similarity index 65% rename from ElementX/Sources/Other/Activity/ActivityPresentable.swift rename to ElementX/Sources/Other/UserIndicators/UserIndicatorViewPresentable.swift index 5bb489fcc..32833da6e 100644 --- a/ElementX/Sources/Other/Activity/ActivityPresentable.swift +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorViewPresentable.swift @@ -16,10 +16,10 @@ import Foundation -/// A presenter associated with and called by an `Activity`, and responsible for the underlying view shown on the screen. -public protocol ActivityPresentable { - /// Called when the `Activity` is started (manually or by the `ActivityCenter`) +/// A presenter associated with and called by a `UserIndicator`, and responsible for the underlying view shown on the screen. +public protocol UserIndicatorViewPresentable { + /// Called when the `UserIndicator` is started (manually or by the `UserIndicatorQueue`) func present() - /// Called when the `Activity` is manually cancelled or completed + /// Called when the `UserIndicator` is manually cancelled or completed func dismiss() } diff --git a/UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift b/UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift new file mode 100644 index 000000000..c140613df --- /dev/null +++ b/UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import ElementX + +class UserIndicatorPresenterSpy: UserIndicatorViewPresentable { + var intel = [String]() + + func present() { + intel.append(#function) + } + + func dismiss() { + intel.append(#function) + } +} diff --git a/UnitTests/Sources/UserIndicators/UserIndicatorQueueTests.swift b/UnitTests/Sources/UserIndicators/UserIndicatorQueueTests.swift new file mode 100644 index 000000000..c8a2835fe --- /dev/null +++ b/UnitTests/Sources/UserIndicators/UserIndicatorQueueTests.swift @@ -0,0 +1,56 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import ElementX + +class UserIndicatorQueueTests: XCTestCase { + var indicators: [UserIndicator]! + var queue: UserIndicatorQueue! + + override func setUp() { + indicators = [] + queue = UserIndicatorQueue() + } + + func makeRequest() -> UserIndicatorRequest { + return UserIndicatorRequest( + presenter: UserIndicatorPresenterSpy(), + dismissal: .manual + ) + } + + func testStartsIndicatorWhenAdded() { + let indicator = queue.add(makeRequest()) + XCTAssertEqual(indicator.state, .executing) + } + + func testSecondIndicatorIsPending() { + queue.add(makeRequest()).store(in: &indicators) + let indicator = queue.add(makeRequest()) + XCTAssertEqual(indicator.state, .pending) + } + + func testSecondIndicatorIsExecutingWhenFirstCompleted() { + let first = queue.add(makeRequest()) + let second = queue.add(makeRequest()) + + first.cancel() + + XCTAssertEqual(second.state, .executing) + } +} diff --git a/UnitTests/Sources/UserIndicators/UserIndicatorTests.swift b/UnitTests/Sources/UserIndicators/UserIndicatorTests.swift new file mode 100644 index 000000000..79db5cb99 --- /dev/null +++ b/UnitTests/Sources/UserIndicators/UserIndicatorTests.swift @@ -0,0 +1,128 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import ElementX + +class UserIndicatorTests: XCTestCase { + var presenter: UserIndicatorPresenterSpy! + + override func setUp() { + super.setUp() + presenter = UserIndicatorPresenterSpy() + } + + func makeIndicator(dismissal: UserIndicatorDismissal = .manual, callback: @escaping () -> Void = {}) -> UserIndicator { + let request = UserIndicatorRequest( + presenter: presenter, + dismissal: dismissal + ) + return UserIndicator( + request: request, + completion: callback + ) + } + + // MARK: - State + + func testNewIndicatorIsPending() { + let indicator = makeIndicator() + XCTAssertEqual(indicator.state, .pending) + } + + func testStartedIndicatorIsExecuting() { + let indicator = makeIndicator() + indicator.start() + XCTAssertEqual(indicator.state, .executing) + } + + func testCancelledIndicatorIsCompleted() { + let indicator = makeIndicator() + indicator.cancel() + XCTAssertEqual(indicator.state, .completed) + } + + // MARK: - Presenter + + func testStartingIndicatorPresentsUI() { + let indicator = makeIndicator() + indicator.start() + XCTAssertEqual(presenter.intel, ["present()"]) + } + + func testAllowStartingOnlyOnce() { + let indicator = makeIndicator() + indicator.start() + presenter.intel = [] + + indicator.start() + + XCTAssertEqual(presenter.intel, []) + } + + func testCancellingIndicatorDismissesUI() { + let indicator = makeIndicator() + indicator.start() + presenter.intel = [] + + indicator.cancel() + + XCTAssertEqual(presenter.intel, ["dismiss()"]) + } + + func testAllowCancellingOnlyOnce() { + let indicator = makeIndicator() + indicator.start() + indicator.cancel() + presenter.intel = [] + + indicator.cancel() + + XCTAssertEqual(presenter.intel, []) + } + + // MARK: - Dismissal + + func testDismissAfterTimeout() { + let interval: TimeInterval = 0.01 + let indicator = makeIndicator(dismissal: .timeout(interval)) + + indicator.start() + + let exp = expectation(description: "") + DispatchQueue.main.asyncAfter(deadline: .now() + interval) { + exp.fulfill() + } + waitForExpectations(timeout: 1) + + XCTAssertEqual(indicator.state, .completed) + } + + // MARK: - Completion callback + + func testTriggersCallbackWhenCompleted() { + var didComplete = false + let indicator = makeIndicator { + didComplete = true + } + indicator.start() + + indicator.cancel() + + XCTAssertTrue(didComplete) + } +}