Add RTE formatting buttons (#1614)

* Add RTE formatting buttons

* Update UUID as stored properties

* Disable autocorrection on URL fields

* Alert ids to let

* Add ComposerToolbar_Previews

* Cleanup

* Cleanup

* Refactor FormatItem colors

* Fix composer layout issue

* Fix ui tests

* Nest ComposerToolbar A11y ids under RoomScreen

* Add composer reply ui tests

* Add UTs

---------

Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com>
This commit is contained in:
aringenbach
2023-09-05 17:39:54 +02:00
committed by GitHub
parent ad5fffe0ed
commit 90545b179e
47 changed files with 863 additions and 47 deletions

View File

@@ -45,6 +45,7 @@
0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */; };
0E8C480700870BB34A2A360F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; };
0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; };
0ED691ADC9C2EA457E7A9427 /* FormattingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */; };
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; };
0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; };
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; };
@@ -870,6 +871,7 @@
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenModels.swift; sourceTree = "<group>"; };
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattingToolbar.swift; sourceTree = "<group>"; };
0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; };
0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = "<group>"; };
0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModel.swift; sourceTree = "<group>"; };
@@ -2265,6 +2267,7 @@
isa = PBXGroup;
children = (
D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */,
0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */,
A0A01AECFF54281CF35909A6 /* MessageComposer.swift */,
1756F24C1913F809A0039FD0 /* MessageComposerTextField.swift */,
3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */,
@@ -4518,6 +4521,7 @@
5CE74302A0725F56F1E9D2A0 /* FormRow.swift in Sources */,
4166A7DD2A4E2EFF0EB9369B /* FormRowLabelStyle.swift in Sources */,
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */,
0ED691ADC9C2EA457E7A9427 /* FormattingToolbar.swift in Sources */,
85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */,
46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */,
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */,

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "bold.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.80005 19C8.25005 19 7.77922 18.8042 7.38755 18.4125C6.99588 18.0208 6.80005 17.55 6.80005 17V7C6.80005 6.45 6.99588 5.97917 7.38755 5.5875C7.77922 5.19583 8.25005 5 8.80005 5H12.325C13.4084 5 14.4084 5.33333 15.325 6C16.2417 6.66667 16.7 7.59167 16.7 8.775C16.7 9.625 16.5084 10.2792 16.125 10.7375C15.7417 11.1958 15.3834 11.525 15.05 11.725C15.4667 11.9083 15.9292 12.25 16.4375 12.75C16.9459 13.25 17.2 14 17.2 15C17.2 16.4833 16.6584 17.5208 15.575 18.1125C14.4917 18.7042 13.475 19 12.525 19H8.80005ZM9.82505 16.2H12.425C13.225 16.2 13.7125 15.9958 13.8875 15.5875C14.0625 15.1792 14.15 14.8833 14.15 14.7C14.15 14.5167 14.0625 14.2208 13.8875 13.8125C13.7125 13.4042 13.2 13.2 12.35 13.2H9.82505V16.2ZM9.82505 10.5H12.15C12.7 10.5 13.1 10.3583 13.35 10.075C13.6 9.79167 13.725 9.475 13.725 9.125C13.725 8.725 13.5834 8.4 13.3 8.15C13.0167 7.9 12.65 7.775 12.2 7.775H9.82505V10.5Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1019 B

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "bullet-list.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 19C9.71667 19 9.47917 18.9042 9.2875 18.7125C9.09583 18.5208 9 18.2833 9 18C9 17.7167 9.09583 17.4792 9.2875 17.2875C9.47917 17.0958 9.71667 17 10 17H20C20.2833 17 20.5208 17.0958 20.7125 17.2875C20.9042 17.4792 21 17.7167 21 18C21 18.2833 20.9042 18.5208 20.7125 18.7125C20.5208 18.9042 20.2833 19 20 19H10ZM10 13C9.71667 13 9.47917 12.9042 9.2875 12.7125C9.09583 12.5208 9 12.2833 9 12C9 11.7167 9.09583 11.4792 9.2875 11.2875C9.47917 11.0958 9.71667 11 10 11H20C20.2833 11 20.5208 11.0958 20.7125 11.2875C20.9042 11.4792 21 11.7167 21 12C21 12.2833 20.9042 12.5208 20.7125 12.7125C20.5208 12.9042 20.2833 13 20 13H10ZM10 7C9.71667 7 9.47917 6.90417 9.2875 6.7125C9.09583 6.52083 9 6.28333 9 6C9 5.71667 9.09583 5.47917 9.2875 5.2875C9.47917 5.09583 9.71667 5 10 5H20C20.2833 5 20.5208 5.09583 20.7125 5.2875C20.9042 5.47917 21 5.71667 21 6C21 6.28333 20.9042 6.52083 20.7125 6.7125C20.5208 6.90417 20.2833 7 20 7H10ZM5 20C4.45 20 3.97917 19.8042 3.5875 19.4125C3.19583 19.0208 3 18.55 3 18C3 17.45 3.19583 16.9792 3.5875 16.5875C3.97917 16.1958 4.45 16 5 16C5.55 16 6.02083 16.1958 6.4125 16.5875C6.80417 16.9792 7 17.45 7 18C7 18.55 6.80417 19.0208 6.4125 19.4125C6.02083 19.8042 5.55 20 5 20ZM5 14C4.45 14 3.97917 13.8042 3.5875 13.4125C3.19583 13.0208 3 12.55 3 12C3 11.45 3.19583 10.9792 3.5875 10.5875C3.97917 10.1958 4.45 10 5 10C5.55 10 6.02083 10.1958 6.4125 10.5875C6.80417 10.9792 7 11.45 7 12C7 12.55 6.80417 13.0208 6.4125 13.4125C6.02083 13.8042 5.55 14 5 14ZM5 8C4.45 8 3.97917 7.80417 3.5875 7.4125C3.19583 7.02083 3 6.55 3 6C3 5.45 3.19583 4.97917 3.5875 4.5875C3.97917 4.19583 4.45 4 5 4C5.55 4 6.02083 4.19583 6.4125 4.5875C6.80417 4.97917 7 5.45 7 6C7 6.55 6.80417 7.02083 6.4125 7.4125C6.02083 7.80417 5.55 8 5 8Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "code-block.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.825 12L10.3 10.525C10.5 10.325 10.6 10.0917 10.6 9.825C10.6 9.55833 10.5 9.325 10.3 9.125C10.1 8.925 9.8625 8.825 9.5875 8.825C9.3125 8.825 9.075 8.925 8.875 9.125L6.7 11.3C6.6 11.4 6.52917 11.5083 6.4875 11.625C6.44583 11.7417 6.425 11.8667 6.425 12C6.425 12.1333 6.44583 12.2583 6.4875 12.375C6.52917 12.4917 6.6 12.6 6.7 12.7L8.875 14.875C9.075 15.075 9.3125 15.175 9.5875 15.175C9.8625 15.175 10.1 15.075 10.3 14.875C10.5 14.675 10.6 14.4417 10.6 14.175C10.6 13.9083 10.5 13.675 10.3 13.475L8.825 12ZM15.175 12L13.7 13.475C13.5 13.675 13.4 13.9083 13.4 14.175C13.4 14.4417 13.5 14.675 13.7 14.875C13.9 15.075 14.1375 15.175 14.4125 15.175C14.6875 15.175 14.925 15.075 15.125 14.875L17.3 12.7C17.4 12.6 17.4708 12.4917 17.5125 12.375C17.5542 12.2583 17.575 12.1333 17.575 12C17.575 11.8667 17.5542 11.7417 17.5125 11.625C17.4708 11.5083 17.4 11.4 17.3 11.3L15.125 9.125C15.025 9.025 14.9125 8.95 14.7875 8.9C14.6625 8.85 14.5375 8.825 14.4125 8.825C14.2875 8.825 14.1625 8.85 14.0375 8.9C13.9125 8.95 13.8 9.025 13.7 9.125C13.5 9.325 13.4 9.55833 13.4 9.825C13.4 10.0917 13.5 10.325 13.7 10.525L15.175 12ZM5 21C4.45 21 3.97917 20.8042 3.5875 20.4125C3.19583 20.0208 3 19.55 3 19V5C3 4.45 3.19583 3.97917 3.5875 3.5875C3.97917 3.19583 4.45 3 5 3H19C19.55 3 20.0208 3.19583 20.4125 3.5875C20.8042 3.97917 21 4.45 21 5V19C21 19.55 20.8042 20.0208 20.4125 20.4125C20.0208 20.8042 19.55 21 19 21H5ZM5 19H19V5H5V19Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "indent.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 21C3.71667 21 3.47917 20.9042 3.2875 20.7125C3.09583 20.5208 3 20.2833 3 20C3 19.7167 3.09583 19.4792 3.2875 19.2875C3.47917 19.0958 3.71667 19 4 19H20C20.2833 19 20.5208 19.0958 20.7125 19.2875C20.9042 19.4792 21 19.7167 21 20C21 20.2833 20.9042 20.5208 20.7125 20.7125C20.5208 20.9042 20.2833 21 20 21H4ZM12 17C11.7167 17 11.4792 16.9042 11.2875 16.7125C11.0958 16.5208 11 16.2833 11 16C11 15.7167 11.0958 15.4792 11.2875 15.2875C11.4792 15.0958 11.7167 15 12 15H20C20.2833 15 20.5208 15.0958 20.7125 15.2875C20.9042 15.4792 21 15.7167 21 16C21 16.2833 20.9042 16.5208 20.7125 16.7125C20.5208 16.9042 20.2833 17 20 17H12ZM12 13C11.7167 13 11.4792 12.9042 11.2875 12.7125C11.0958 12.5208 11 12.2833 11 12C11 11.7167 11.0958 11.4792 11.2875 11.2875C11.4792 11.0958 11.7167 11 12 11H20C20.2833 11 20.5208 11.0958 20.7125 11.2875C20.9042 11.4792 21 11.7167 21 12C21 12.2833 20.9042 12.5208 20.7125 12.7125C20.5208 12.9042 20.2833 13 20 13H12ZM12 9C11.7167 9 11.4792 8.90417 11.2875 8.7125C11.0958 8.52083 11 8.28333 11 8C11 7.71667 11.0958 7.47917 11.2875 7.2875C11.4792 7.09583 11.7167 7 12 7H20C20.2833 7 20.5208 7.09583 20.7125 7.2875C20.9042 7.47917 21 7.71667 21 8C21 8.28333 20.9042 8.52083 20.7125 8.7125C20.5208 8.90417 20.2833 9 20 9H12ZM4 5C3.71667 5 3.47917 4.90417 3.2875 4.7125C3.09583 4.52083 3 4.28333 3 4C3 3.71667 3.09583 3.47917 3.2875 3.2875C3.47917 3.09583 3.71667 3 4 3H20C20.2833 3 20.5208 3.09583 20.7125 3.2875C20.9042 3.47917 21 3.71667 21 4C21 4.28333 20.9042 4.52083 20.7125 4.7125C20.5208 4.90417 20.2833 5 20 5H4ZM3.85 15.15C3.68333 15.3167 3.5 15.3583 3.3 15.275C3.1 15.1917 3 15.0333 3 14.8V9.2C3 8.96667 3.1 8.80833 3.3 8.725C3.5 8.64167 3.68333 8.68333 3.85 8.85L6.65 11.65C6.75 11.75 6.8 11.8667 6.8 12C6.8 12.1333 6.75 12.25 6.65 12.35L3.85 15.15Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "inline-code.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.9578 5.62066C15.1165 5.09167 14.8163 4.53419 14.2873 4.37549C13.7583 4.21679 13.2008 4.51698 13.0421 5.04597L9.04213 18.3793C8.88344 18.9083 9.18362 19.4658 9.71261 19.6245C10.2416 19.7832 10.7991 19.483 10.9578 18.954L14.9578 5.62066Z" fill="#656D77"/>
<path d="M5.97352 7.23184C5.54924 6.87828 4.91868 6.9356 4.56511 7.35988L1.23178 11.3599C0.92274 11.7307 0.92274 12.2694 1.23178 12.6402L4.56511 16.6402C4.91868 17.0645 5.54924 17.1218 5.97352 16.7683C6.3978 16.4147 6.45512 15.7842 6.10155 15.3599L3.30171 12.0001L6.10155 8.64025C6.45512 8.21597 6.3978 7.58541 5.97352 7.23184Z" fill="#656D77"/>
<path d="M18.0265 7.23184C18.4508 6.87828 19.0813 6.9356 19.4349 7.35988L22.7682 11.3599C23.0773 11.7307 23.0773 12.2694 22.7682 12.6402L19.4349 16.6402C19.0813 17.0645 18.4508 17.1218 18.0265 16.7683C17.6022 16.4147 17.5449 15.7842 17.8984 15.3599L20.6983 12.0001L17.8984 8.64025C17.5449 8.21597 17.6022 7.58541 18.0265 7.23184Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "italic.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.25 19C5.9 19 5.60417 18.8792 5.3625 18.6375C5.12083 18.3958 5 18.1 5 17.75C5 17.4 5.12083 17.1042 5.3625 16.8625C5.60417 16.6208 5.9 16.5 6.25 16.5H9L12 7.5H9.25C8.9 7.5 8.60417 7.37917 8.3625 7.1375C8.12083 6.89583 8 6.6 8 6.25C8 5.9 8.12083 5.60417 8.3625 5.3625C8.60417 5.12083 8.9 5 9.25 5H16.75C17.1 5 17.3958 5.12083 17.6375 5.3625C17.8792 5.60417 18 5.9 18 6.25C18 6.6 17.8792 6.89583 17.6375 7.1375C17.3958 7.37917 17.1 7.5 16.75 7.5H14.5L11.5 16.5H13.75C14.1 16.5 14.3958 16.6208 14.6375 16.8625C14.8792 17.1042 15 17.4 15 17.75C15 18.1 14.8792 18.3958 14.6375 18.6375C14.3958 18.8792 14.1 19 13.75 19H6.25Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "link.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19.0711C11.0218 20.0493 9.84331 20.5384 8.46445 20.5384C7.08559 20.5384 5.90708 20.0493 4.92892 19.0711C3.95075 18.0929 3.46167 16.9144 3.46167 15.5356C3.46167 14.1567 3.95075 12.9782 4.92892 12L7.05024 9.87872C7.25058 9.67837 7.48629 9.5782 7.75734 9.5782C8.0284 9.5782 8.2641 9.67837 8.46445 9.87872C8.6648 10.0791 8.76497 10.3148 8.76497 10.5858C8.76497 10.8569 8.6648 11.0926 8.46445 11.2929L6.34313 13.4143C5.75387 14.0035 5.45925 14.7106 5.45925 15.5356C5.45925 16.3605 5.75387 17.0676 6.34313 17.6569C6.93239 18.2461 7.63949 18.5408 8.46445 18.5408C9.28941 18.5408 9.99651 18.2461 10.5858 17.6569L12.7071 15.5356C12.9074 15.3352 13.1431 15.2351 13.4142 15.2351C13.6853 15.2351 13.921 15.3352 14.1213 15.5356C14.3217 15.7359 14.4218 15.9716 14.4218 16.2427C14.4218 16.5137 14.3217 16.7494 14.1213 16.9498L12 19.0711ZM10.5858 14.8285C10.3854 15.0288 10.1497 15.129 9.87866 15.129C9.60761 15.129 9.3719 15.0288 9.17156 14.8285C8.97121 14.6281 8.87104 14.3924 8.87104 14.1214C8.87104 13.8503 8.97121 13.6146 9.17156 13.4143L13.4142 9.17161C13.6145 8.97126 13.8502 8.87109 14.1213 8.87109C14.3924 8.87109 14.6281 8.97126 14.8284 9.17161C15.0288 9.37196 15.1289 9.60766 15.1289 9.87872C15.1289 10.1498 15.0288 10.3855 14.8284 10.5858L10.5858 14.8285ZM16.9497 14.1214C16.7494 14.3217 16.5137 14.4219 16.2426 14.4219C15.9716 14.4219 15.7359 14.3217 15.5355 14.1214C15.3352 13.921 15.235 13.6853 15.235 13.4143C15.235 13.1432 15.3352 12.9075 15.5355 12.7071L17.6568 10.5858C18.2461 9.99657 18.5407 9.28946 18.5407 8.4645C18.5407 7.63955 18.2461 6.93244 17.6568 6.34318C17.0676 5.75393 16.3605 5.4593 15.5355 5.4593C14.7106 5.4593 14.0035 5.75393 13.4142 6.34318L11.2929 8.4645C11.0925 8.66485 10.8568 8.76502 10.5858 8.76502C10.3147 8.76502 10.079 8.66485 9.87866 8.4645C9.67832 8.26416 9.57814 8.02845 9.57814 7.7574C9.57814 7.48634 9.67832 7.25064 9.87866 7.05029L12 4.92897C12.9781 3.95081 14.1567 3.46172 15.5355 3.46172C16.9144 3.46172 18.0929 3.95081 19.0711 4.92897C20.0492 5.90713 20.5383 7.08565 20.5383 8.4645C20.5383 9.84336 20.0492 11.0219 19.0711 12L16.9497 14.1214Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "numbered-list.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.75 22C3.53333 22 3.35417 21.9292 3.2125 21.7875C3.07083 21.6458 3 21.4667 3 21.25C3 21.0333 3.07083 20.8542 3.2125 20.7125C3.35417 20.5708 3.53333 20.5 3.75 20.5H5.5V19.75H4.75C4.53333 19.75 4.35417 19.6792 4.2125 19.5375C4.07083 19.3958 4 19.2167 4 19C4 18.7833 4.07083 18.6042 4.2125 18.4625C4.35417 18.3208 4.53333 18.25 4.75 18.25H5.5V17.5H3.75C3.53333 17.5 3.35417 17.4292 3.2125 17.2875C3.07083 17.1458 3 16.9667 3 16.75C3 16.5333 3.07083 16.3542 3.2125 16.2125C3.35417 16.0708 3.53333 16 3.75 16H6C6.28333 16 6.52083 16.0958 6.7125 16.2875C6.90417 16.4792 7 16.7167 7 17V18C7 18.2833 6.90417 18.5208 6.7125 18.7125C6.52083 18.9042 6.28333 19 6 19C6.28333 19 6.52083 19.0958 6.7125 19.2875C6.90417 19.4792 7 19.7167 7 20V21C7 21.2833 6.90417 21.5208 6.7125 21.7125C6.52083 21.9042 6.28333 22 6 22H3.75ZM3.75 15C3.53333 15 3.35417 14.9292 3.2125 14.7875C3.07083 14.6458 3 14.4667 3 14.25V12.25C3 11.9667 3.09583 11.7292 3.2875 11.5375C3.47917 11.3458 3.71667 11.25 4 11.25H5.5V10.5H3.75C3.53333 10.5 3.35417 10.4292 3.2125 10.2875C3.07083 10.1458 3 9.96667 3 9.75C3 9.53333 3.07083 9.35417 3.2125 9.2125C3.35417 9.07083 3.53333 9 3.75 9H6C6.28333 9 6.52083 9.09583 6.7125 9.2875C6.90417 9.47917 7 9.71667 7 10V11.75C7 12.0333 6.90417 12.2708 6.7125 12.4625C6.52083 12.6542 6.28333 12.75 6 12.75H4.5V13.5H6.25C6.46667 13.5 6.64583 13.5708 6.7875 13.7125C6.92917 13.8542 7 14.0333 7 14.25C7 14.4667 6.92917 14.6458 6.7875 14.7875C6.64583 14.9292 6.46667 15 6.25 15H3.75ZM5.25 8C5.03333 8 4.85417 7.92917 4.7125 7.7875C4.57083 7.64583 4.5 7.46667 4.5 7.25V3.5H3.75C3.53333 3.5 3.35417 3.42917 3.2125 3.2875C3.07083 3.14583 3 2.96667 3 2.75C3 2.53333 3.07083 2.35417 3.2125 2.2125C3.35417 2.07083 3.53333 2 3.75 2H5.25C5.46667 2 5.64583 2.07083 5.7875 2.2125C5.92917 2.35417 6 2.53333 6 2.75V7.25C6 7.46667 5.92917 7.64583 5.7875 7.7875C5.64583 7.92917 5.46667 8 5.25 8ZM10 19C9.71667 19 9.47917 18.9042 9.2875 18.7125C9.09583 18.5208 9 18.2833 9 18C9 17.7167 9.09583 17.4792 9.2875 17.2875C9.47917 17.0958 9.71667 17 10 17H20C20.2833 17 20.5208 17.0958 20.7125 17.2875C20.9042 17.4792 21 17.7167 21 18C21 18.2833 20.9042 18.5208 20.7125 18.7125C20.5208 18.9042 20.2833 19 20 19H10ZM10 13C9.71667 13 9.47917 12.9042 9.2875 12.7125C9.09583 12.5208 9 12.2833 9 12C9 11.7167 9.09583 11.4792 9.2875 11.2875C9.47917 11.0958 9.71667 11 10 11H20C20.2833 11 20.5208 11.0958 20.7125 11.2875C20.9042 11.4792 21 11.7167 21 12C21 12.2833 20.9042 12.5208 20.7125 12.7125C20.5208 12.9042 20.2833 13 20 13H10ZM10 7C9.71667 7 9.47917 6.90417 9.2875 6.7125C9.09583 6.52083 9 6.28333 9 6C9 5.71667 9.09583 5.47917 9.2875 5.2875C9.47917 5.09583 9.71667 5 10 5H20C20.2833 5 20.5208 5.09583 20.7125 5.2875C20.9042 5.47917 21 5.71667 21 6C21 6.28333 20.9042 6.52083 20.7125 6.7125C20.5208 6.90417 20.2833 7 20 7H10Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "quote.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.7187 4.34006C4.81333 3.69825 4.35307 3.10363 3.69069 3.01195C3.02831 2.92026 2.41464 3.36622 2.32001 4.00803L1.51233 9.4862C1.41771 10.128 1.87796 10.7226 2.54034 10.8143C3.20272 10.906 3.81639 10.46 3.91102 9.81823L4.7187 4.34006Z" fill="#656D77"/>
<path d="M16.8339 14.5138C16.9285 13.872 16.4683 13.2774 15.8059 13.1857C15.1435 13.094 14.5298 13.54 14.4352 14.1818L13.6275 19.6599C13.5329 20.3018 13.9932 20.8964 14.6555 20.9881C15.3179 21.0797 15.9316 20.6338 16.0262 19.992L16.8339 14.5138Z" fill="#656D77"/>
<path d="M9.31836 3.00862C9.98263 3.08634 10.4561 3.67112 10.3759 4.31477L10.3538 4.48993C10.3396 4.60182 10.319 4.76312 10.2934 4.96135C10.242 5.35767 10.1702 5.90238 10.0884 6.49553C9.92685 7.66729 9.72013 9.07483 9.55343 9.88243C9.42221 10.5182 8.78395 10.9305 8.12784 10.8033C7.47173 10.6762 7.04622 10.0577 7.17744 9.42199C7.32617 8.70148 7.52328 7.36993 7.68671 6.18464C7.76742 5.59926 7.83839 5.06095 7.88919 4.66888C7.91458 4.47292 7.93491 4.31368 7.94887 4.20358L7.97026 4.0339C8.05047 3.39025 8.65408 2.93089 9.31836 3.00862Z" fill="#656D77"/>
<path d="M22.4877 14.5138C22.5823 13.872 22.122 13.2774 21.4597 13.1857C20.7973 13.094 20.1836 13.54 20.089 14.1818L19.2813 19.6599C19.1867 20.3018 19.6469 20.8964 20.3093 20.9881C20.9717 21.0797 21.5854 20.6338 21.68 19.992L22.4877 14.5138Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "strikethrough.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.15 20C10.8833 20 9.75833 19.625 8.775 18.875C7.79167 18.125 7.08333 17.1 6.65 15.8L8.85 14.85C9.08333 15.65 9.4875 16.3083 10.0625 16.825C10.6375 17.3417 11.35 17.6 12.2 17.6C12.9 17.6 13.5333 17.4333 14.1 17.1C14.6667 16.7667 14.95 16.2333 14.95 15.5C14.95 15.2 14.8917 14.925 14.775 14.675C14.6583 14.425 14.5 14.2 14.3 14H17.1C17.1833 14.2333 17.2458 14.4708 17.2875 14.7125C17.3292 14.9542 17.35 15.2167 17.35 15.5C17.35 16.9333 16.8375 18.0417 15.8125 18.825C14.7875 19.6083 13.5667 20 12.15 20ZM3 12C2.71667 12 2.47917 11.9042 2.2875 11.7125C2.09583 11.5208 2 11.2833 2 11C2 10.7167 2.09583 10.4792 2.2875 10.2875C2.47917 10.0958 2.71667 10 3 10H21C21.2833 10 21.5208 10.0958 21.7125 10.2875C21.9042 10.4792 22 10.7167 22 11C22 11.2833 21.9042 11.5208 21.7125 11.7125C21.5208 11.9042 21.2833 12 21 12H3ZM12.05 3.85C13.15 3.85 14.1125 4.12083 14.9375 4.6625C15.7625 5.20417 16.4 6.03333 16.85 7.15L14.65 8.125C14.5 7.64166 14.2208 7.20833 13.8125 6.825C13.4042 6.44166 12.8333 6.25 12.1 6.25C11.4167 6.25 10.85 6.40417 10.4 6.7125C9.95 7.02083 9.7 7.45 9.65 8H7.25C7.28333 6.85 7.7375 5.87083 8.6125 5.0625C9.4875 4.25417 10.6333 3.85 12.05 3.85Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "text-format.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2320_116500)">
<path d="M3 20.6667C3 21.4 3.6 22 4.33333 22H20.3333C21.0667 22 21.6667 21.4 21.6667 20.6667C21.6667 19.9333 21.0667 19.3333 20.3333 19.3333H4.33333C3.6 19.3333 3 19.9333 3 20.6667ZM9 13.7333H15.6667L16.5467 15.8667C16.7467 16.3467 17.2133 16.6667 17.7333 16.6667C18.6533 16.6667 19.2667 15.72 18.9067 14.88L13.7333 2.92C13.4933 2.36 12.9467 2 12.3333 2C11.72 2 11.1733 2.36 10.9333 2.92L5.76 14.88C5.4 15.72 6.02667 16.6667 6.94667 16.6667C7.46667 16.6667 7.93333 16.3467 8.13333 15.8667L9 13.7333ZM12.3333 4.64L14.8267 11.3333H9.84L12.3333 4.64Z" fill="#1B1D22"/>
</g>
<defs>
<clipPath id="clip0_2320_116500">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 819 B

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "underline.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 21C5.71667 21 5.47917 20.9042 5.2875 20.7125C5.09583 20.5208 5 20.2833 5 20C5 19.7167 5.09583 19.4792 5.2875 19.2875C5.47917 19.0958 5.71667 19 6 19H18C18.2833 19 18.5208 19.0958 18.7125 19.2875C18.9042 19.4792 19 19.7167 19 20C19 20.2833 18.9042 20.5208 18.7125 20.7125C18.5208 20.9042 18.2833 21 18 21H6ZM12 17C10.3167 17 9.00833 16.475 8.075 15.425C7.14167 14.375 6.675 12.9833 6.675 11.25V4.275C6.675 3.925 6.80417 3.625 7.0625 3.375C7.32083 3.125 7.625 3 7.975 3C8.325 3 8.625 3.125 8.875 3.375C9.125 3.625 9.25 3.925 9.25 4.275V11.4C9.25 12.3333 9.48333 13.0917 9.95 13.675C10.4167 14.2583 11.1 14.55 12 14.55C12.9 14.55 13.5833 14.2583 14.05 13.675C14.5167 13.0917 14.75 12.3333 14.75 11.4V4.275C14.75 3.925 14.8792 3.625 15.1375 3.375C15.3958 3.125 15.7 3 16.05 3C16.4 3 16.7 3.125 16.95 3.375C17.2 3.625 17.325 3.925 17.325 4.275V11.25C17.325 12.9833 16.8583 14.375 15.925 15.425C14.9917 16.475 13.6833 17 12 17Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "unindent.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 21C3.71667 21 3.47917 20.9042 3.2875 20.7125C3.09583 20.5208 3 20.2833 3 20C3 19.7167 3.09583 19.4792 3.2875 19.2875C3.47917 19.0958 3.71667 19 4 19H20C20.2833 19 20.5208 19.0958 20.7125 19.2875C20.9042 19.4792 21 19.7167 21 20C21 20.2833 20.9042 20.5208 20.7125 20.7125C20.5208 20.9042 20.2833 21 20 21H4ZM12 17C11.7167 17 11.4792 16.9042 11.2875 16.7125C11.0958 16.5208 11 16.2833 11 16C11 15.7167 11.0958 15.4792 11.2875 15.2875C11.4792 15.0958 11.7167 15 12 15H20C20.2833 15 20.5208 15.0958 20.7125 15.2875C20.9042 15.4792 21 15.7167 21 16C21 16.2833 20.9042 16.5208 20.7125 16.7125C20.5208 16.9042 20.2833 17 20 17H12ZM12 13C11.7167 13 11.4792 12.9042 11.2875 12.7125C11.0958 12.5208 11 12.2833 11 12C11 11.7167 11.0958 11.4792 11.2875 11.2875C11.4792 11.0958 11.7167 11 12 11H20C20.2833 11 20.5208 11.0958 20.7125 11.2875C20.9042 11.4792 21 11.7167 21 12C21 12.2833 20.9042 12.5208 20.7125 12.7125C20.5208 12.9042 20.2833 13 20 13H12ZM12 9C11.7167 9 11.4792 8.90417 11.2875 8.7125C11.0958 8.52083 11 8.28333 11 8C11 7.71667 11.0958 7.47917 11.2875 7.2875C11.4792 7.09583 11.7167 7 12 7H20C20.2833 7 20.5208 7.09583 20.7125 7.2875C20.9042 7.47917 21 7.71667 21 8C21 8.28333 20.9042 8.52083 20.7125 8.7125C20.5208 8.90417 20.2833 9 20 9H12ZM4 5C3.71667 5 3.47917 4.90417 3.2875 4.7125C3.09583 4.52083 3 4.28333 3 4C3 3.71667 3.09583 3.47917 3.2875 3.2875C3.47917 3.09583 3.71667 3 4 3H20C20.2833 3 20.5208 3.09583 20.7125 3.2875C20.9042 3.47917 21 3.71667 21 4C21 4.28333 20.9042 4.52083 20.7125 4.7125C20.5208 4.90417 20.2833 5 20 5H4ZM6.15 15.15L3.35 12.35C3.25 12.25 3.2 12.1333 3.2 12C3.2 11.8667 3.25 11.75 3.35 11.65L6.15 8.85C6.31667 8.68333 6.5 8.64167 6.7 8.725C6.9 8.80833 7 8.96667 7 9.2V14.8C7 15.0333 6.9 15.1917 6.7 15.275C6.5 15.3583 6.31667 15.3167 6.15 15.15Z" fill="#656D77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -32,6 +32,19 @@ internal enum Asset {
internal static let appLogo = ImageAsset(name: "images/app-logo")
internal static let serverSelectionIcon = ImageAsset(name: "images/server-selection-icon")
internal static let closeCircle = ImageAsset(name: "images/close-circle")
internal static let bold = ImageAsset(name: "images/bold")
internal static let bulletList = ImageAsset(name: "images/bullet-list")
internal static let codeBlock = ImageAsset(name: "images/code-block")
internal static let indent = ImageAsset(name: "images/indent")
internal static let inlineCode = ImageAsset(name: "images/inline-code")
internal static let italic = ImageAsset(name: "images/italic")
internal static let link = ImageAsset(name: "images/link")
internal static let numberedList = ImageAsset(name: "images/numbered-list")
internal static let quote = ImageAsset(name: "images/quote")
internal static let strikethrough = ImageAsset(name: "images/strikethrough")
internal static let textFormat = ImageAsset(name: "images/text-format")
internal static let underline = ImageAsset(name: "images/underline")
internal static let unindent = ImageAsset(name: "images/unindent")
internal static let encryptionNormal = ImageAsset(name: "images/encryption-normal")
internal static let encryptionTrusted = ImageAsset(name: "images/encryption-trusted")
internal static let encryptionWarning = ImageAsset(name: "images/encryption-warning")

View File

@@ -34,6 +34,7 @@ struct RoomProxyMockConfiguration {
var members: [RoomMemberProxyProtocol]?
var inviter: RoomMemberProxyMock?
var memberForID: RoomMemberProxyMock = .mockMe
var ownUserID = "@alice:somewhere.org"
var invitedMembersCount = 100
var joinedMembersCount = 50
@@ -61,6 +62,7 @@ extension RoomProxyMock {
invitedMembersCount = configuration.invitedMembersCount
joinedMembersCount = configuration.joinedMembersCount
activeMembersCount = configuration.activeMembersCount
ownUserID = configuration.ownUserID
if let members = configuration.members {
membersPublisher = Just(members).eraseToAnyPublisher()
@@ -78,5 +80,6 @@ extension RoomProxyMock {
setNameClosure = { _ in .success(()) }
setTopicClosure = { _ in .success(()) }
getMemberUserIDReturnValue = .success(configuration.memberForID)
canUserRedactUserIDReturnValue = .success(false)
}
}

View File

@@ -106,12 +106,32 @@ struct A11yIdentifiers {
struct RoomScreen {
let name = "room-name"
let avatar = "room-avatar"
let attachmentPicker = "room-attachment_picker"
let attachmentPickerPhotoLibrary = "room-attachment_picker_photo_library"
let attachmentPickerDocuments = "room-attachment_picker_documents"
let attachmentPickerCamera = "room-attachment_picker_camera"
let attachmentPickerLocation = "room-attachment_picker_location"
let attachmentPickerPoll = "room-attachment_picker_poll"
let attachmentPickerTextFormatting = "room-attachment_picker_text_formatting"
let timelineItemActionMenu = "room-timeline_item_action_menu"
let composerToolbar = ComposerToolbar()
struct ComposerToolbar {
let bold = "composer_toolbar-bold"
let italic = "composer_toolbar-italic"
let underline = "composer_toolbar-underline"
let strikethrough = "composer_toolbar-strikethrough"
let unorderedList = "composer_toolbar-unordered_list"
let orderedList = "composer_toolbar-ordered_list"
let indent = "composer_toolbar-indent"
let unindent = "composer_toolbar-unindent"
let inlineCode = "composer_toolbar-inline_code"
let codeBlock = "composer_toolbar-code_block"
let quote = "composer_toolbar-quote"
let link = "composer_toolbar-link"
let openComposeOptions = "composer_toolbar-open_compose_options"
let closeFormattingOptions = "composer_toolbar-close-formatting-options"
}
}
struct RoomDetailsScreen {

View File

@@ -52,12 +52,21 @@ extension View {
/// .alert(item: $context.alertInfo)
/// ```
struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
struct AlertButton {
struct AlertButton: Identifiable {
let id = UUID()
let title: String
var role: ButtonRole?
let action: (() -> Void)?
}
struct AlertTextField: Identifiable {
let id = UUID()
let placeholder: String
let text: Binding<String>
let autoCapitalization: TextInputAutocapitalization
let autoCorrectionDisabled: Bool
}
/// An identifier that can be used to distinguish one error from another.
let id: T
/// The alert's title.
@@ -68,6 +77,10 @@ struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
var primaryButton = AlertButton(title: L10n.actionOk, action: nil)
/// The alert's secondary button title and action.
var secondaryButton: AlertButton?
/// The alert's displayed text fields.
var textFields: [AlertTextField]?
/// The alert's additional buttons displayed vertically above the primary button.
var verticalButtons: [AlertButton]?
}
extension AlertInfo {
@@ -95,9 +108,28 @@ extension AlertInfo {
extension View {
func alert<T: Hashable>(item: Binding<AlertInfo<T>?>) -> some View {
alert(item: item) { item in
if let verticalButtons = item.verticalButtons {
ForEach(verticalButtons) { button in
Button(button.title, role: button.role) {
button.action?()
}
}
}
if let textFields = item.textFields {
VStack(spacing: 24) {
ForEach(textFields) { textField in
TextField(textField.placeholder, text: textField.text)
.textInputAutocapitalization(textField.autoCapitalization)
.autocorrectionDisabled(textField.autoCorrectionDisabled)
}
}
}
Button(item.primaryButton.title, role: item.primaryButton.role) {
item.primaryButton.action?()
}
if let secondaryButton = item.secondaryButton {
Button(secondaryButton.title, role: secondaryButton.role) {
secondaryButton.action?()

View File

@@ -14,7 +14,9 @@
// limitations under the License.
//
import SwiftUI
import UIKit
import WysiwygComposer
enum ComposerToolbarViewModelAction {
case sendMessage(plain: String, html: String, mode: RoomScreenComposerMode)
@@ -43,6 +45,8 @@ enum ComposerToolbarViewAction {
case displayLocationPicker
case displayPollForm
case handlePasteOrDrop(provider: NSItemProvider)
case enableTextFormatting
case composerAction(action: ComposerAction)
}
struct ComposerToolbarViewState: BindableState {
@@ -61,8 +65,11 @@ struct ComposerToolbarViewState: BindableState {
}
struct ComposerToolbarViewStateBindings {
var composerPlainText: String
var composerFocused: Bool
var composerPlainText = ""
var composerFocused = false
var composerActionsEnabled = false
var formatItems: [FormatItem] = .init()
var alertInfo: AlertInfo<UUID>?
var showAttachmentPopover = false {
didSet {
@@ -72,3 +79,167 @@ struct ComposerToolbarViewStateBindings {
}
}
}
/// An item in the toolbar
struct FormatItem {
/// The type of the item
let type: FormatType
/// The state of the item
let state: ActionState
}
/// The types of formatting actions
enum FormatType {
case bold
case italic
case underline
case strikeThrough
case unorderedList
case orderedList
case indent
case unindent
case inlineCode
case codeBlock
case quote
case link
}
extension FormatType: CaseIterable, Identifiable {
var id: Self { self }
}
extension FormatItem: Identifiable {
var id: FormatType { type }
}
extension FormatItem {
/// The icon to display in the formatting toolbar.
var icon: Image {
switch type {
case .bold:
return Image(asset: Asset.Images.bold)
case .italic:
return Image(asset: Asset.Images.italic)
case .underline:
return Image(asset: Asset.Images.underline)
case .strikeThrough:
return Image(asset: Asset.Images.strikethrough)
case .unorderedList:
return Image(asset: Asset.Images.bulletList)
case .orderedList:
return Image(asset: Asset.Images.numberedList)
case .indent:
return Image(asset: Asset.Images.indent)
case .unindent:
return Image(asset: Asset.Images.unindent)
case .inlineCode:
return Image(asset: Asset.Images.inlineCode)
case .codeBlock:
return Image(asset: Asset.Images.codeBlock)
case .quote:
return Image(asset: Asset.Images.quote)
case .link:
return Image(asset: Asset.Images.link)
}
}
var accessibilityIdentifier: String {
switch type {
case .bold:
return A11yIdentifiers.roomScreen.composerToolbar.bold
case .italic:
return A11yIdentifiers.roomScreen.composerToolbar.italic
case .underline:
return A11yIdentifiers.roomScreen.composerToolbar.underline
case .strikeThrough:
return A11yIdentifiers.roomScreen.composerToolbar.strikethrough
case .unorderedList:
return A11yIdentifiers.roomScreen.composerToolbar.unorderedList
case .orderedList:
return A11yIdentifiers.roomScreen.composerToolbar.orderedList
case .indent:
return A11yIdentifiers.roomScreen.composerToolbar.indent
case .unindent:
return A11yIdentifiers.roomScreen.composerToolbar.unindent
case .inlineCode:
return A11yIdentifiers.roomScreen.composerToolbar.inlineCode
case .codeBlock:
return A11yIdentifiers.roomScreen.composerToolbar.codeBlock
case .quote:
return A11yIdentifiers.roomScreen.composerToolbar.quote
case .link:
return A11yIdentifiers.roomScreen.composerToolbar.link
}
}
var accessibilityLabel: String {
switch type {
case .bold:
return L10n.richTextEditorFormatBold
case .italic:
return L10n.richTextEditorFormatItalic
case .underline:
return L10n.richTextEditorFormatUnderline
case .strikeThrough:
return L10n.richTextEditorFormatStrikethrough
case .unorderedList:
return L10n.richTextEditorBulletList
case .orderedList:
return L10n.richTextEditorNumberedList
case .indent:
return L10n.richTextEditorIndent
case .unindent:
return L10n.richTextEditorUnindent
case .inlineCode:
return L10n.richTextEditorInlineCode
case .codeBlock:
return L10n.richTextEditorCodeBlock
case .quote:
return L10n.richTextEditorQuote
case .link:
return L10n.richTextEditorLink
}
}
}
extension FormatType {
/// The associated library composer action.
var composerAction: ComposerAction {
switch self {
case .bold:
return .bold
case .italic:
return .italic
case .underline:
return .underline
case .strikeThrough:
return .strikeThrough
case .unorderedList:
return .unorderedList
case .orderedList:
return .orderedList
case .indent:
return .indent
case .unindent:
return .unindent
case .inlineCode:
return .inlineCode
case .codeBlock:
return .codeBlock
case .quote:
return .quote
case .link:
return .link
}
}
/// Return true if the format type is an indentation action.
var isIndentType: Bool {
switch self {
case .indent, .unindent:
return true
default:
return false
}
}
}

View File

@@ -15,6 +15,8 @@
//
import Combine
import Foundation
import SwiftUI
import WysiwygComposer
typealias ComposerToolbarViewModelType = StateStoreViewModel<ComposerToolbarViewState, ComposerToolbarViewAction>
@@ -26,10 +28,19 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
actionsSubject.eraseToAnyPublisher()
}
private struct WysiwygLinkData {
let action: LinkAction
let range: NSRange
var url: String
var text: String
}
private var currentLinkData: WysiwygLinkData?
init(wysiwygViewModel: WysiwygComposerViewModel) {
self.wysiwygViewModel = wysiwygViewModel
super.init(initialViewState: ComposerToolbarViewState(bindings: .init(composerPlainText: "", composerFocused: false)))
super.init(initialViewState: ComposerToolbarViewState(bindings: .init()))
context.$viewState
.map(\.composerMode)
@@ -46,6 +57,20 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
wysiwygViewModel.$isContentEmpty
.weakAssign(to: \.state.composerEmpty, on: self)
.store(in: &cancellables)
wysiwygViewModel.$actionStates
.map { actions in
FormatType
.allCases
// Exclude indent type outside of lists.
.filter { wysiwygViewModel.isInList || !$0.isIndentType }
.map { type in
FormatItem(type: type,
state: actions[type.composerAction] ?? .disabled)
}
}
.weakAssign(to: \.state.bindings.formatItems, on: self)
.store(in: &cancellables)
}
// MARK: - Public
@@ -65,6 +90,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
actionsSubject.send(.sendPlainTextMessage(message: context.composerPlainText,
mode: state.composerMode))
}
state.bindings.composerActionsEnabled = false
case .cancelReply:
set(mode: .default)
case .cancelEdit:
@@ -82,6 +108,15 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
actionsSubject.send(.displayPollForm)
case .handlePasteOrDrop(let provider):
actionsSubject.send(.handlePasteOrDrop(provider: provider))
case .enableTextFormatting:
state.bindings.composerActionsEnabled = true
state.bindings.composerFocused = true
case .composerAction(let action):
if action == .link {
createLinkAlert()
} else {
wysiwygViewModel.apply(action)
}
}
}
@@ -128,4 +163,124 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
state.bindings.composerPlainText = text
}
}
private func createLinkAlert() {
let linkAction = wysiwygViewModel.getLinkAction()
currentLinkData = WysiwygLinkData(action: linkAction,
range: wysiwygViewModel.attributedContent.selection,
url: linkAction.url ?? "",
text: "")
let urlBinding: Binding<String> = .init { [weak self] in
self?.currentLinkData?.url ?? ""
} set: { [weak self] value in
self?.currentLinkData?.url = value
}
let textBinding: Binding<String> = .init { [weak self] in
self?.currentLinkData?.text ?? ""
} set: { [weak self] value in
self?.currentLinkData?.text = value
}
switch linkAction {
case .createWithText:
state.bindings.alertInfo = makeCreateWithTextAlertInfo(urlBinding: urlBinding, textBinding: textBinding)
case .create:
state.bindings.alertInfo = makeSetUrlAlertInfo(urlBinding: urlBinding, isEdit: false)
case .edit:
state.bindings.alertInfo = makeEditChoiceAlertInfo(urlBinding: urlBinding)
case .disabled:
break
}
}
private func makeCreateWithTextAlertInfo(urlBinding: Binding<String>, textBinding: Binding<String>) -> AlertInfo<UUID> {
AlertInfo(id: UUID(),
title: L10n.richTextEditorCreateLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel, action: {
self.restoreComposerSelectedRange()
}),
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave, action: {
self.restoreComposerSelectedRange()
self.createLinkWithText()
}),
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.commonText,
text: textBinding,
autoCapitalization: .never,
autoCorrectionDisabled: false),
AlertInfo<UUID>.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder,
text: urlBinding,
autoCapitalization: .never,
autoCorrectionDisabled: true)])
}
private func makeSetUrlAlertInfo(urlBinding: Binding<String>, isEdit: Bool) -> AlertInfo<UUID> {
AlertInfo(id: UUID(),
title: isEdit ? L10n.richTextEditorEditLink : L10n.richTextEditorCreateLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel, action: {
self.restoreComposerSelectedRange()
}),
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave, action: {
self.restoreComposerSelectedRange()
self.setLink()
}),
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder,
text: urlBinding,
autoCapitalization: .never,
autoCorrectionDisabled: true)])
}
private func makeEditChoiceAlertInfo(urlBinding: Binding<String>) -> AlertInfo<UUID> {
AlertInfo(id: UUID(),
title: L10n.richTextEditorEditLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionRemove, role: .destructive, action: {
self.restoreComposerSelectedRange()
self.removeLinks()
}),
verticalButtons: [AlertInfo<UUID>.AlertButton(title: L10n.actionEdit, action: {
self.state.bindings.alertInfo = nil
DispatchQueue.main.async {
self.state.bindings.alertInfo = self.makeSetUrlAlertInfo(urlBinding: urlBinding, isEdit: true)
}
})])
}
private func restoreComposerSelectedRange() {
guard let currentLinkData else { return }
wysiwygViewModel.select(range: currentLinkData.range)
}
private func setLink() {
guard let currentLinkData else { return }
wysiwygViewModel.applyLinkOperation(.setLink(urlString: currentLinkData.url))
}
private func createLinkWithText() {
guard let currentLinkData else { return }
wysiwygViewModel.applyLinkOperation(.createLink(urlString: currentLinkData.url,
text: currentLinkData.text))
}
private func removeLinks() {
wysiwygViewModel.applyLinkOperation(.removeLinks)
}
}
private extension WysiwygComposerViewModel {
/// Return true if the selection of the composer is currently located in a list.
var isInList: Bool {
actionStates[.orderedList] == .reversed || actionStates[.unorderedList] == .reversed
}
}
private extension LinkAction {
var url: String? {
guard case .edit(let url) = self else {
return nil
}
return url
}
}

View File

@@ -26,29 +26,65 @@ struct ComposerToolbar: View {
@ScaledMetric private var sendButtonIconSize = 16
var body: some View {
VStack(spacing: 8) {
topBar
if context.composerActionsEnabled {
bottomBar
}
}
.alert(item: $context.alertInfo)
}
private var topBar: some View {
HStack(alignment: .bottom, spacing: 10) {
RoomAttachmentPicker(context: context)
.padding(.bottom, 5) // centre align with the send button
if !context.composerActionsEnabled {
RoomAttachmentPicker(context: context)
.padding(.bottom, 5) // centre align with the send button
}
messageComposer
.environmentObject(context)
Button {
context.send(viewAction: .sendMessage)
} label: {
submitButtonImage
.symbolVariant(.fill)
.font(.compound.bodyLG)
.foregroundColor(context.viewState.sendButtonDisabled ? .compound.iconDisabled : .global.white)
.background {
Circle()
.foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary)
}
if !context.composerActionsEnabled {
sendButton
}
.disabled(context.viewState.sendButtonDisabled)
.animation(.linear(duration: 0.1), value: context.viewState.sendButtonDisabled)
.keyboardShortcut(.return, modifiers: [.command])
.padding([.vertical, .trailing], 6)
}
}
private var bottomBar: some View {
HStack(alignment: .bottom, spacing: 10) {
Button {
context.composerActionsEnabled = false
} label: {
Image(systemName: "xmark.circle.fill")
.font(.compound.headingLG)
.foregroundColor(.compound.textActionPrimary)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions)
.padding(.bottom, 5) // centre align with the send button
FormattingToolbar(formatItems: context.formatItems) { action in
context.send(viewAction: .composerAction(action: action.composerAction))
}
sendButton
}
}
private var sendButton: some View {
Button {
context.send(viewAction: .sendMessage)
} label: {
submitButtonImage
.symbolVariant(.fill)
.font(.compound.bodyLG)
.foregroundColor(context.viewState.sendButtonDisabled ? .compound.iconDisabled : .global.white)
.background {
Circle()
.foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary)
}
}
.disabled(context.viewState.sendButtonDisabled)
.animation(.linear(duration: 0.1), value: context.viewState.sendButtonDisabled)
.keyboardShortcut(.return, modifiers: [.command])
.padding([.vertical, .trailing], 6)
}
private var messageComposer: some View {
MessageComposer(plainText: $context.composerPlainText,
@@ -109,6 +145,12 @@ struct ComposerToolbar: View {
}
}
struct ComposerToolbar_Previews: PreviewProvider {
static var previews: some View {
ComposerToolbar.mock()
}
}
// MARK: - Mock
extension ComposerToolbar {

View File

@@ -0,0 +1,68 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct FormattingToolbar: View {
/// The list of items to render in the toolbar
var formatItems: [FormatItem]
/// The action when an item is selected
var formatAction: (FormatType) -> Void
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 4) {
ForEach(formatItems) { item in
Button {
formatAction(item.type)
} label: {
item.icon
.renderingMode(.template)
.foregroundColor(item.foregroundColor)
}
.disabled(item.state == .disabled)
.frame(width: 44, height: 44)
.background(item.backgroundColor)
.cornerRadius(8)
.accessibilityIdentifier(item.accessibilityIdentifier)
.accessibilityLabel(item.accessibilityLabel)
}
}
}
}
}
private extension FormatItem {
var foregroundColor: Color {
switch state {
case .reversed:
return .compound.iconOnSolidPrimary
case .enabled:
return .compound.iconSecondary
case .disabled:
return .compound.iconDisabled
}
}
var backgroundColor: Color {
switch state {
case .reversed:
return .compound.bgActionPrimaryRest
default:
return .compound.bgCanvasDefault
}
}
}

View File

@@ -59,7 +59,7 @@ struct MessageComposer: View {
}
}
}
.padding([.leading, .trailing], 12.0)
.padding(.horizontal, 12.0)
.clipped()
.background {
ZStack {
@@ -107,16 +107,16 @@ private struct MessageComposerReplyHeader: View {
.padding(4.0)
.background(Color.compound.bgCanvasDefault)
.cornerRadius(13.0)
.padding([.trailing, .vertical], 8.0)
.padding([.leading], -4.0)
.overlay(alignment: .topTrailing) {
Button(action: action) {
Image(systemName: "xmark")
.font(.compound.bodySM.weight(.medium))
.foregroundColor(.compound.iconTertiary)
.padding(16.0)
.padding(8.0)
}
}
.padding(.vertical, 8.0)
.padding(.horizontal, -4.0)
}
}

View File

@@ -31,7 +31,7 @@ struct RoomAttachmentPicker: View {
.font(.compound.headingLG)
.foregroundColor(.compound.textActionPrimary)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPicker)
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions)
.popover(isPresented: $context.showAttachmentPopover) {
VStack(alignment: .leading, spacing: 0.0) {
Button {
@@ -72,6 +72,17 @@ struct RoomAttachmentPicker: View {
} label: {
PickerLabel(title: L10n.screenRoomAttachmentSourcePoll, icon: Image(asset: Asset.Images.timelinePollAttachment))
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll)
if ServiceLocator.shared.settings.richTextEditorEnabled {
Button {
context.showAttachmentPopover = false
context.send(viewAction: .enableTextFormatting)
} label: {
PickerLabel(title: L10n.screenRoomAttachmentTextFormatting, icon: Image(asset: Asset.Images.textFormat))
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerTextFormatting)
}
}
.padding(.top, isPresented ? 20 : 0)
.background {

View File

@@ -368,7 +368,7 @@ class MockScreen: Identifiable {
var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5))
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy)
return SessionVerificationScreenCoordinator(parameters: parameters)
case .userSessionScreen:
case .userSessionScreen, .userSessionScreenReply:
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
let clientProxy = MockClientProxy(userID: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms)))

View File

@@ -50,6 +50,7 @@ enum UITestsScreenIdentifier: String {
case roomWithUndisclosedPolls
case sessionVerification
case userSessionScreen
case userSessionScreenReply
case roomDetailsScreen
case roomDetailsScreenWithRoomAvatar
case roomDetailsScreenWithEmptyTopic

View File

@@ -59,13 +59,13 @@ class UserFlowTests: XCTestCase {
for identifier in [A11yIdentifiers.roomScreen.attachmentPickerPhotoLibrary,
A11yIdentifiers.roomScreen.attachmentPickerDocuments,
A11yIdentifiers.roomScreen.attachmentPickerLocation] {
tapOnButton(A11yIdentifiers.roomScreen.attachmentPicker)
tapOnButton(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions)
tapOnButton(identifier)
tapOnButton("Cancel")
}
// Open attachments picker
tapOnButton(A11yIdentifiers.roomScreen.attachmentPicker)
tapOnButton(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions)
// Open photo library picker
tapOnButton(A11yIdentifiers.roomScreen.attachmentPickerPhotoLibrary)

View File

@@ -21,21 +21,28 @@ import XCTest
class UserSessionScreenTests: XCTestCase {
func testUserSessionFlows() async throws {
let roomName = "First room"
let app = Application.launch(.userSessionScreen)
try await app.assertScreenshot(.userSessionScreen, step: 1)
app.buttons[A11yIdentifiers.homeScreen.roomName(roomName)].tap()
XCTAssert(app.staticTexts[roomName].waitForExistence(timeout: 5.0))
try await Task.sleep(for: .seconds(1))
try await app.assertScreenshot(.userSessionScreen, step: 2)
app.buttons[A11yIdentifiers.roomScreen.attachmentPicker].tap()
app.buttons[A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions].tap()
try await app.assertScreenshot(.userSessionScreen, step: 3)
}
func testUserSessionReply() async throws {
let roomName = "First room"
let app = Application.launch(.userSessionScreenReply, disableTimelineAccessibility: false)
app.buttons[A11yIdentifiers.homeScreen.roomName(roomName)].tap()
XCTAssert(app.staticTexts[roomName].waitForExistence(timeout: 5.0))
try await Task.sleep(for: .seconds(1))
let cell = app.cells.firstMatch
cell.swipeRight(velocity: .fast)
try await app.assertScreenshot(.userSessionScreenReply)
}
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d62a74c1b78bd48824bf06d97344c49f30fb5db1bd1c87b960e8f5ce20a0227
size 330298

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:33826c63e307b6990c1b890699f0aebfc855819c8d9dabeea76d65c2cf6dd64c
size 291077

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f508eff9ade01d498119147fd543ffdd363a40fa2cc2865710eb59283813e5d0
size 332447

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:51a6d371dc7b105205e5aea957b084752106c7d046098a84c15f7dcdde9742dc
size 291289

View File

@@ -20,9 +20,20 @@ import XCTest
@MainActor
class ComposerToolbarViewModelTests: XCTestCase {
private var appSettings: AppSettings!
private var wysiwygViewModel: WysiwygComposerViewModel!
private var viewModel: ComposerToolbarViewModel!
override func setUp() {
AppSettings.reset()
appSettings = AppSettings()
appSettings.richTextEditorEnabled = true
ServiceLocator.shared.register(appSettings: appSettings)
wysiwygViewModel = WysiwygComposerViewModel()
viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel)
}
func testComposerFocus() {
let wysiwygViewModel = WysiwygComposerViewModel()
let viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel)
viewModel.process(roomAction: .setMode(mode: .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock"))))
XCTAssertTrue(viewModel.state.bindings.composerFocused)
viewModel.process(roomAction: .removeFocus)
@@ -30,8 +41,6 @@ class ComposerToolbarViewModelTests: XCTestCase {
}
func testComposerMode() {
let wysiwygViewModel = WysiwygComposerViewModel()
let viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel)
let mode: RoomScreenComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock"))
viewModel.process(roomAction: .setMode(mode: mode))
XCTAssertEqual(viewModel.state.composerMode, mode)
@@ -40,8 +49,6 @@ class ComposerToolbarViewModelTests: XCTestCase {
}
func testComposerModeIsPublished() {
let wysiwygViewModel = WysiwygComposerViewModel()
let viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel)
let mode: RoomScreenComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock"))
let expectation = expectation(description: "Composer mode is published")
let cancellable = viewModel
@@ -62,9 +69,27 @@ class ComposerToolbarViewModelTests: XCTestCase {
}
func testHandleKeyCommand() {
let wysiwygViewModel = WysiwygComposerViewModel()
let viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel)
XCTAssertTrue(viewModel.handleKeyCommand(.enter))
XCTAssertFalse(viewModel.handleKeyCommand(.shiftEnter))
}
func testComposerFocusAfterEnablingRTE() {
viewModel.process(viewAction: .enableTextFormatting)
XCTAssertTrue(viewModel.state.bindings.composerFocused)
}
func testRTEDisabledAfterSendingMessage() {
viewModel.process(viewAction: .enableTextFormatting)
XCTAssertTrue(viewModel.state.bindings.composerFocused)
viewModel.state.composerEmpty = false
viewModel.process(viewAction: .sendMessage)
XCTAssertFalse(viewModel.state.bindings.composerActionsEnabled)
}
func testAlertIsShownAfterLinkAction() {
XCTAssertNil(viewModel.state.bindings.alertInfo)
viewModel.process(viewAction: .enableTextFormatting)
viewModel.process(viewAction: .composerAction(action: .link))
XCTAssertNotNil(viewModel.state.bindings.alertInfo)
}
}