App polish part 2 (#1118)

- Fix sheet backgrounds in dark mode.
- Fix background colours for reaction buttons.
- Reply view layout.
- Update separator colours.
- Don't show sender name/avatar in DMs.
- Fix inconsistent line heights with formatted body text.
- Use plain text for reply
- Increase tappable size of collapsed state changes button.
- Blockquote layout and text colour.
- Tap to expand the topic in room details.
- Change the topic and security font size in room details.
- Add cancel button when inviting someone to an existing room.
- Reword Add Friends to Add People in start chat screen.
- Update compound.
This commit is contained in:
Doug
2023-06-20 18:31:40 +01:00
committed by GitHub
parent 2f091cf8d1
commit ede4ab095c
30 changed files with 141 additions and 95 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ let package = Package(
.library(name: "DesignKit", targets: ["DesignKit"])
],
dependencies: [
.package(url: "https://github.com/vector-im/compound-ios.git", revision: "d59c317362beba940baa43d6aacdd357e208048d"),
.package(url: "https://github.com/vector-im/compound-ios.git", revision: "e8b35fdd8c4008079dfce203e63bf7a05582d7b9"),
.package(url: "https://github.com/vector-im/element-design-tokens.git", exact: "0.0.3"),
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4")
],
+1 -1
View File
@@ -4980,7 +4980,7 @@
repositoryURL = "https://github.com/vector-im/compound-ios";
requirement = {
kind = revision;
revision = d59c317362beba940baa43d6aacdd357e208048d;
revision = e8b35fdd8c4008079dfce203e63bf7a05582d7b9;
};
};
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
@@ -11,9 +11,9 @@
{
"identity" : "compound-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vector-im/compound-ios",
"location" : "https://github.com/vector-im/compound-ios.git",
"state" : {
"revision" : "d59c317362beba940baa43d6aacdd357e208048d"
"revision" : "e8b35fdd8c4008079dfce203e63bf7a05582d7b9"
}
},
{
@@ -181,7 +181,7 @@
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334",
"version" : "1.11.0"
@@ -27,6 +27,7 @@
"action_invite" = "Invite";
"action_invite_friends" = "Invite friends";
"action_invite_friends_to_app" = "Invite friends to %1$@";
"action_invite_people_to_app" = "Invite people to %1$@";
"action_invites_list" = "Invites";
"action_learn_more" = "Learn more";
"action_leave" = "Leave";
+4
View File
@@ -68,6 +68,10 @@ public enum L10n {
public static func actionInviteFriendsToApp(_ p1: Any) -> String {
return L10n.tr("Localizable", "action_invite_friends_to_app", String(describing: p1))
}
/// Invite people to %1$@
public static func actionInvitePeopleToApp(_ p1: Any) -> String {
return L10n.tr("Localizable", "action_invite_people_to_app", String(describing: p1))
}
/// Invites
public static var actionInvitesList: String { return L10n.tr("Localizable", "action_invites_list") }
/// Learn more
@@ -38,15 +38,17 @@
// On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI"
// In case of "Times New Roman" fallback, use system font and reuse UIFontDescriptorSymbolicTraits.
if ([font.familyName.lowercaseString containsString:@"times"])
{
if ([font.familyName.lowercaseString containsString:@"times"]) {
UIFontDescriptorSymbolicTraits symbolicTraits = (UIFontDescriptorSymbolicTraits)CTFontGetSymbolicTraits(ctFont);
UIFontDescriptor *systemFontDescriptor = [UIFont systemFontOfSize:fontSize].fontDescriptor;
// Start with the body text style and update it to keep consistent line spacing with plain text messages.
UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];
fontDescriptor = [fontDescriptor fontDescriptorWithSize:fontSize];
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
UIFontDescriptor *finalFontDescriptor = [systemFontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
font = [UIFont fontWithDescriptor:finalFontDescriptor size:fontSize];
font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize];
}
return font;
}
+2 -5
View File
@@ -34,11 +34,8 @@ private struct InteractiveDismissSearchModifier: ViewModifier {
@Environment(\.isSearching) private var isSearching
func body(content: Content) -> some View {
if isSearching {
content.interactiveDismissDisabled()
} else {
content
}
content
.interactiveDismissDisabled(isSearching)
}
}
@@ -51,7 +51,7 @@ struct AnalyticsPromptScreen: View {
.foregroundColor(.compound.textSecondary)
Divider()
.overlay { Color.compound._borderRowSeparator }
.overlay { Color.compound.borderDisabled }
.padding(.vertical, 20)
checkmarkList
@@ -188,7 +188,7 @@ struct CreateRoomScreen: View {
.formSectionHeader()
.padding(.top, 40)
}
.listRowSeparatorTint(.compound._borderRowSeparator)
.listRowSeparatorTint(.compound.borderDisabled)
.listRowBackground(Color.element.formRowBackground)
}
@@ -41,7 +41,7 @@ struct HomeScreenRoomCell: View {
.padding(.vertical, verticalInsets)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color.compound._borderRowSeparator)
.fill(Color.compound.borderDisabled)
.frame(height: 1 / UIScreen.main.scale)
.padding(.trailing, -horizontalInsets)
}
@@ -26,6 +26,7 @@ struct InviteUsersScreenCoordinatorParameters {
}
enum InviteUsersScreenCoordinatorAction {
case cancel
case proceed
case invite(users: [String])
case toggleUser(UserProfileProxy)
@@ -55,12 +56,14 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
actionsSubject.send(.cancel)
case .proceed:
self.actionsSubject.send(.proceed)
actionsSubject.send(.proceed)
case .invite(let users):
self.actionsSubject.send(.invite(users: users))
actionsSubject.send(.invite(users: users))
case .toggleUser(let user):
self.actionsSubject.send(.toggleUser(user))
actionsSubject.send(.toggleUser(user))
}
}
.store(in: &cancellables)
@@ -22,6 +22,7 @@ enum InviteUsersScreenErrorType: Error {
}
enum InviteUsersScreenViewModelAction {
case cancel
case proceed
case invite(users: [String])
case toggleUser(UserProfileProxy)
@@ -86,6 +87,7 @@ struct InviteUsersScreenViewStateBindings {
}
enum InviteUsersScreenViewAction {
case cancel
case proceed
case toggleUser(UserProfileProxy)
}
@@ -51,6 +51,8 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
override func process(viewAction: InviteUsersScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.cancel)
case .proceed:
switch roomType {
case .draft:
@@ -26,11 +26,7 @@ struct InviteUsersScreen: View {
.scrollDismissesKeyboard(.immediately)
.navigationTitle(L10n.screenCreateRoomAddPeopleTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
nextButton
}
}
.toolbar { toolbar }
.disableInteractiveDismissOnSearch()
.dismissSearchOnDisappear()
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: L10n.commonSearchForSomeone)
@@ -123,11 +119,22 @@ struct InviteUsersScreen: View {
.frame(width: frame.width)
}
private var nextButton: some View {
Button { context.send(viewAction: .proceed) } label: {
Text(context.viewState.actionText)
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
if !context.viewState.isCreatingRoom {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
ToolbarItem(placement: .confirmationAction) {
Button(context.viewState.actionText) {
context.send(viewAction: .proceed)
}
.disabled(context.viewState.isActionDisabled)
}
.disabled(context.viewState.isActionDisabled)
}
private func deselect(_ user: UserProfileProxy) {
@@ -116,7 +116,7 @@ struct InvitesScreenCell: View {
private var separator: some View {
Rectangle()
.fill(Color.compound._borderRowSeparator)
.fill(Color.compound.borderDisabled)
.frame(height: 1 / UIScreen.main.scale)
}
@@ -91,10 +91,9 @@ struct RoomDetailsEditScreen: View {
prompt: canEditName ? Text(L10n.commonRoomNamePlaceholder) : nil,
axis: .horizontal)
.focused($focus, equals: .name)
.font(.compound.bodyLG)
.foregroundColor(.compound.textPrimary)
.textFieldStyle(.compoundForm)
.disabled(!canEditName)
.listRowBackground(canEditName ? nil : Color.clear)
.listRowBackground(canEditName ? Color.element.formRowBackground : .clear)
.clipShape(RoundedRectangle(cornerRadius: 8))
} header: {
Text(L10n.commonRoomName)
@@ -109,14 +108,13 @@ struct RoomDetailsEditScreen: View {
TextField(L10n.commonTopic,
text: $context.topic,
prompt: canEditTopic ? Text(L10n.commonTopicPlaceholder) : nil,
prompt: canEditTopic ? Text(L10n.commonTopicPlaceholder).foregroundColor(.compound.textPlaceholder) : nil,
axis: .vertical)
.focused($focus, equals: .topic)
.font(.compound.bodyLG)
.foregroundColor(.compound.textPrimary)
.textFieldStyle(.compoundForm)
.disabled(!canEditTopic)
.listRowBackground(canEditTopic ? nil : Color.clear)
.lineLimit(3, reservesSpace: true)
.listRowBackground(canEditTopic ? Color.element.formRowBackground : .clear)
.lineLimit(3...)
} header: {
Text(L10n.commonTopic)
.formSectionHeader()
@@ -104,6 +104,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch result {
case .cancel:
navigationStackCoordinator?.setSheetCoordinator(nil)
case .proceed:
break
case .invite(let users):
@@ -114,7 +116,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
}
.store(in: &cancellables)
parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) { [weak self] in
navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) { [weak self] in
self?.selectedUsers.value = []
}
}
@@ -19,6 +19,8 @@ import SwiftUI
struct RoomDetailsScreen: View {
@ObservedObject var context: RoomDetailsScreenViewModel.Context
@State private var isTopicExpanded = false
var body: some View {
Form {
if let recipient = context.viewState.dmRecipient {
@@ -111,8 +113,9 @@ struct RoomDetailsScreen: View {
if let topic = context.viewState.topic, !topic.isEmpty {
Text(topic)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodySM)
.lineLimit(3)
.font(.compound.bodyMD)
.lineLimit(isTopicExpanded ? nil : 3)
.onTapGesture { isTopicExpanded.toggle() }
} else {
Button {
context.send(viewAction: .processTapAddTopic)
@@ -155,7 +158,7 @@ struct RoomDetailsScreen: View {
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
}
}
.listRowSeparatorTint(.compound._borderRowSeparator)
.listRowSeparatorTint(.compound.borderDisabled)
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
.formSectionStyle()
.foregroundColor(.compound.textPrimary)
@@ -247,7 +250,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
.mockCharlie
]
let roomProxy = RoomProxyMock(with: .init(displayName: "Room A",
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
topic: "Bacon ipsum dolor amet short ribs buffalo pork loin cupim frankfurter. Burgdoggen pig shankle biltong flank ham jowl sirloin bacon cow. T-bone alcatra boudin beef spare ribs pig fatback jerky swine short ribs shankle chislic frankfurter pork loin. Chicken tri-tip bresaola t-bone pastrami brisket.", // swiftlint:disable:this line_length
isDirect: false,
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
@@ -86,6 +86,7 @@ struct RoomScreenViewState: BindableState {
var showLoading = false
var timelineStyle: TimelineStyle
var readReceiptsEnabled: Bool
var isEncryptedOneToOneRoom = false
var bindings: RoomScreenViewStateBindings
@@ -45,6 +45,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
roomAvatarURL: roomProxy.avatarURL,
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
readReceiptsEnabled: ServiceLocator.shared.settings.readReceiptsEnabled,
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
bindings: .init(composerText: "", composerFocused: false)),
imageProvider: mediaProvider)
@@ -101,29 +101,41 @@ struct TimelineReplyView: View {
var icon: Icon?
var isTextOnly: Bool {
icon?.mediaSource == nil && icon?.systemIconName == nil
}
/// The string shown as the message preview.
///
/// This converts the formatted body to a plain string to remove formatting
/// and render with a consistent font size. This conversion is done to avoid
/// showing markdown characters in the preview for messages with formatting.
var messagePreview: String {
guard let formattedBody else { return plainBody }
return String(formattedBody.characters)
}
var body: some View {
HStack {
HStack(spacing: 8) {
iconView
.frame(width: imageContainerSize, height: imageContainerSize)
.foregroundColor(.compound.iconPrimary)
.background(Color.compound.bgSubtlePrimary)
.cornerRadius(icon?.cornerRadii ?? 0.0, corners: .allCorners)
if icon?.mediaSource == nil, icon?.systemIconName == nil {
Spacer().frame(width: 4.0)
}
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: 2) {
Text(sender.displayName ?? sender.id)
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textPrimary)
Text(formattedBody ?? AttributedString(plainBody))
Text(messagePreview)
.font(.compound.bodyMD)
.foregroundColor(.compound.textPlaceholder)
.foregroundColor(.compound.textSecondary)
.tint(.compound.textLinkExternal)
.lineLimit(2)
}
.padding(.leading, isTextOnly ? 8 : 0)
.padding(.trailing, 8)
}
}
@@ -154,7 +166,7 @@ struct TimelineReplyView_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock
static var previews: some View {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 20) {
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: ""))
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loading(eventID: ""))
@@ -65,8 +65,8 @@ struct RoomAttachmentPicker: View {
}
}
}
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.presentationDetents([.height(sheetContentHeight)])
.presentationBackground(Color.compound.bgCanvasDefault)
.presentationDragIndicator(.visible)
}
}
@@ -31,14 +31,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
@State private var showItemActionMenu = false
private var isTextItem: Bool {
timelineItem is TextBasedRoomTimelineItem
}
private var isTextItem: Bool { timelineItem is TextBasedRoomTimelineItem }
private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom }
var body: some View {
ZStack(alignment: .trailingFirstTextBaseline) {
VStack(alignment: alignment, spacing: -12) {
if !timelineItem.isOutgoing {
if !timelineItem.isOutgoing, !isEncryptedOneToOneRoom {
header
.zIndex(1)
}
@@ -213,7 +212,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
// The rendered reply bubble with a greedy width. The custom layout prevents
// the infinite width from increasing the overall width of the view.
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.foregroundColor(.compound.textPlaceholder)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -235,7 +233,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
}
private var messageBubbleTopPadding: CGFloat {
guard timelineItem.isOutgoing else { return 0 }
guard timelineItem.isOutgoing || isEncryptedOneToOneRoom else { return 0 }
return timelineGroupStyle == .single || timelineGroupStyle == .first ? 8 : 0
}
@@ -298,13 +296,14 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
}
static var mockTimeline: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.state.items) { item in
item.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.state.items) { item in
item.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
}
}
}
.environment(\.timelineStyle, .bubbles)
.previewLayout(.sizeThatFits)
.environmentObject(viewModel.context)
}
@@ -53,7 +53,7 @@ struct TimelineReactionButton: View {
.background(
backgroundShape
.strokeBorder(reaction.isHighlighted ? Color.compound.textSecondary : .compound.bgCanvasDefault, lineWidth: 2)
.background(reaction.isHighlighted ? Color.compound.textPrimary.opacity(0.1) : .compound.bgSubtleSecondary, in: backgroundShape)
.background(reaction.isHighlighted ? Color.compound.textPrimary.opacity(0.1) : .compound._bgReactionButton, in: backgroundShape)
)
.accessibilityElement(children: .combine)
}
@@ -42,33 +42,25 @@ struct CollapsibleRoomTimelineView: View {
}
}
struct CollapsibleRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
let item = CollapsibleTimelineItem(items: [SeparatorRoomTimelineItem(id: "First separator", text: "This is a separator"),
SeparatorRoomTimelineItem(id: "Second separator", text: "This is another separator")])
CollapsibleRoomTimelineView(timelineItem: item)
}
}
private struct CollapsibleRoomTimelineItemDisclosureGroupStyle: DisclosureGroupStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(spacing: 0.0) {
HStack(alignment: .center) {
configuration.label
Text(Image(systemName: "chevron.forward"))
.rotationEffect(.degrees(configuration.isExpanded ? 90 : 0))
.animation(.elementDefault, value: configuration.isExpanded)
Button { configuration.isExpanded.toggle() } label: {
HStack(alignment: .center) {
configuration.label
Text(Image(systemName: "chevron.forward"))
.rotationEffect(.degrees(configuration.isExpanded ? 90 : 0))
.animation(.elementDefault, value: configuration.isExpanded)
}
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.padding(.horizontal, 36.0)
.padding(.vertical, 12.0)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.padding(.horizontal, 36.0)
.padding(.top, 20.0)
.padding(.bottom, 12.0)
.onTapGesture {
configuration.isExpanded.toggle()
}
.padding(.top, 8.0)
if configuration.isExpanded {
configuration.content
@@ -76,3 +68,14 @@ private struct CollapsibleRoomTimelineItemDisclosureGroupStyle: DisclosureGroupS
}
}
}
struct CollapsibleRoomTimelineView_Previews: PreviewProvider {
static let item = CollapsibleTimelineItem(items: [
SeparatorRoomTimelineItem(id: "First separator", text: "This is a separator"),
SeparatorRoomTimelineItem(id: "Second separator", text: "This is another separator")
])
static var previews: some View {
CollapsibleRoomTimelineView(timelineItem: item)
}
}
@@ -57,16 +57,17 @@ struct FormattedBodyText: View {
// The rendered blockquote with a greedy width. The custom layout prevents the
// infinite width from increasing the overall width of the view.
Text(component.attributedString.mergingAttributes(blockquoteAttributes))
.foregroundColor(.compound.textPlaceholder)
.foregroundColor(.compound.textSecondary)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 12.0)
.overlay(alignment: .leading) {
// User an overlay here so that the rectangle's infinite height doesn't take priority
Rectangle()
Capsule()
.frame(width: 2.0)
.padding(.leading, 6.0)
.foregroundColor(.compound.textPlaceholder)
.padding(.leading, 5.0)
.foregroundColor(.compound.textSecondary)
.padding(.vertical, 2)
}
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
} else {
@@ -152,6 +153,12 @@ struct FormattedBodyText_Previews: PreviewProvider {
<blockquote>Third blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>
""",
"""
<blockquote>A blockquote that is long and goes onto multiple lines as the first item in the message</blockquote>
<p>Then another line of text here to reply to the blockquote, which is also a multiline component.</p>
<blockquote>Short line here.</blockquote>
<p>And a simple reply here.</p>
""",
"""
<code>Hello world</code>
<p>Text</p>
<code><b>Hello</b> <i>world</i></code>
@@ -86,8 +86,8 @@ public struct TimelineItemMenu: View {
}
}
}
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.presentationDetents([.medium, .large])
.presentationBackground(Color.compound.bgCanvasDefault)
.presentationDragIndicator(.visible)
}
@@ -164,7 +164,7 @@ public struct TimelineItemMenu: View {
private func reactionBackgroundColor(for emoji: String) -> Color {
if item.properties.reactions.first(where: { $0.key == emoji }) != nil {
return .compound._bgReactionButton
return .compound.bgActionPrimaryRest
} else {
return .clear
}
@@ -97,6 +97,8 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch result {
case .cancel:
break // Not shown in this flow.
case .proceed:
openCreateRoomScreen()
case .invite:
@@ -79,7 +79,7 @@ struct StartChatScreen: View {
private var inviteFriendsSection: some View {
Section {
MatrixUserShareLink(userID: context.viewState.userID) {
Label(L10n.actionInviteFriendsToApp(InfoPlistReader.main.bundleDisplayName), systemImage: "square.and.arrow.up")
Label(L10n.actionInvitePeopleToApp(InfoPlistReader.main.bundleDisplayName), systemImage: "square.and.arrow.up")
}
.buttonStyle(FormButtonStyle())
.accessibilityIdentifier(A11yIdentifiers.startChatScreen.inviteFriends)
+1 -1
View File
@@ -53,7 +53,7 @@ packages:
minorVersion: 5.13.0
Compound:
url: https://github.com/vector-im/compound-ios
revision: d59c317362beba940baa43d6aacdd357e208048d
revision: e8b35fdd8c4008079dfce203e63bf7a05582d7b9
# path: ../compound-ios
Algorithms:
url: https://github.com/apple/swift-algorithms