mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
@@ -12,7 +12,7 @@ let package = Package(
|
|||||||
.library(name: "DesignKit", targets: ["DesignKit"])
|
.library(name: "DesignKit", targets: ["DesignKit"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
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/vector-im/element-design-tokens.git", exact: "0.0.3"),
|
||||||
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4")
|
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4")
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4980,7 +4980,7 @@
|
|||||||
repositoryURL = "https://github.com/vector-im/compound-ios";
|
repositoryURL = "https://github.com/vector-im/compound-ios";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = revision;
|
kind = revision;
|
||||||
revision = d59c317362beba940baa43d6aacdd357e208048d;
|
revision = e8b35fdd8c4008079dfce203e63bf7a05582d7b9;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
{
|
{
|
||||||
"identity" : "compound-ios",
|
"identity" : "compound-ios",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/vector-im/compound-ios",
|
"location" : "https://github.com/vector-im/compound-ios.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "d59c317362beba940baa43d6aacdd357e208048d"
|
"revision" : "e8b35fdd8c4008079dfce203e63bf7a05582d7b9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
{
|
{
|
||||||
"identity" : "swift-snapshot-testing",
|
"identity" : "swift-snapshot-testing",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
|
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334",
|
"revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334",
|
||||||
"version" : "1.11.0"
|
"version" : "1.11.0"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"action_invite" = "Invite";
|
"action_invite" = "Invite";
|
||||||
"action_invite_friends" = "Invite friends";
|
"action_invite_friends" = "Invite friends";
|
||||||
"action_invite_friends_to_app" = "Invite friends to %1$@";
|
"action_invite_friends_to_app" = "Invite friends to %1$@";
|
||||||
|
"action_invite_people_to_app" = "Invite people to %1$@";
|
||||||
"action_invites_list" = "Invites";
|
"action_invites_list" = "Invites";
|
||||||
"action_learn_more" = "Learn more";
|
"action_learn_more" = "Learn more";
|
||||||
"action_leave" = "Leave";
|
"action_leave" = "Leave";
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ public enum L10n {
|
|||||||
public static func actionInviteFriendsToApp(_ p1: Any) -> String {
|
public static func actionInviteFriendsToApp(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "action_invite_friends_to_app", String(describing: p1))
|
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
|
/// Invites
|
||||||
public static var actionInvitesList: String { return L10n.tr("Localizable", "action_invites_list") }
|
public static var actionInvitesList: String { return L10n.tr("Localizable", "action_invites_list") }
|
||||||
/// Learn more
|
/// Learn more
|
||||||
|
|||||||
@@ -38,15 +38,17 @@
|
|||||||
|
|
||||||
// On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI"
|
// On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI"
|
||||||
// In case of "Times New Roman" fallback, use system font and reuse UIFontDescriptorSymbolicTraits.
|
// 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);
|
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:fontDescriptor size:fontSize];
|
||||||
font = [UIFont fontWithDescriptor:finalFontDescriptor size:fontSize];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return font;
|
return font;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,8 @@ private struct InteractiveDismissSearchModifier: ViewModifier {
|
|||||||
@Environment(\.isSearching) private var isSearching
|
@Environment(\.isSearching) private var isSearching
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if isSearching {
|
content
|
||||||
content.interactiveDismissDisabled()
|
.interactiveDismissDisabled(isSearching)
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ struct AnalyticsPromptScreen: View {
|
|||||||
.foregroundColor(.compound.textSecondary)
|
.foregroundColor(.compound.textSecondary)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.overlay { Color.compound._borderRowSeparator }
|
.overlay { Color.compound.borderDisabled }
|
||||||
.padding(.vertical, 20)
|
.padding(.vertical, 20)
|
||||||
|
|
||||||
checkmarkList
|
checkmarkList
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ struct CreateRoomScreen: View {
|
|||||||
.formSectionHeader()
|
.formSectionHeader()
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
}
|
}
|
||||||
.listRowSeparatorTint(.compound._borderRowSeparator)
|
.listRowSeparatorTint(.compound.borderDisabled)
|
||||||
.listRowBackground(Color.element.formRowBackground)
|
.listRowBackground(Color.element.formRowBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ struct HomeScreenRoomCell: View {
|
|||||||
.padding(.vertical, verticalInsets)
|
.padding(.vertical, verticalInsets)
|
||||||
.overlay(alignment: .bottom) {
|
.overlay(alignment: .bottom) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.compound._borderRowSeparator)
|
.fill(Color.compound.borderDisabled)
|
||||||
.frame(height: 1 / UIScreen.main.scale)
|
.frame(height: 1 / UIScreen.main.scale)
|
||||||
.padding(.trailing, -horizontalInsets)
|
.padding(.trailing, -horizontalInsets)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ struct InviteUsersScreenCoordinatorParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenCoordinatorAction {
|
enum InviteUsersScreenCoordinatorAction {
|
||||||
|
case cancel
|
||||||
case proceed
|
case proceed
|
||||||
case invite(users: [String])
|
case invite(users: [String])
|
||||||
case toggleUser(UserProfileProxy)
|
case toggleUser(UserProfileProxy)
|
||||||
@@ -55,12 +56,14 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel.actions.sink { [weak self] action in
|
viewModel.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
|
case .cancel:
|
||||||
|
actionsSubject.send(.cancel)
|
||||||
case .proceed:
|
case .proceed:
|
||||||
self.actionsSubject.send(.proceed)
|
actionsSubject.send(.proceed)
|
||||||
case .invite(let users):
|
case .invite(let users):
|
||||||
self.actionsSubject.send(.invite(users: users))
|
actionsSubject.send(.invite(users: users))
|
||||||
case .toggleUser(let user):
|
case .toggleUser(let user):
|
||||||
self.actionsSubject.send(.toggleUser(user))
|
actionsSubject.send(.toggleUser(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ enum InviteUsersScreenErrorType: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenViewModelAction {
|
enum InviteUsersScreenViewModelAction {
|
||||||
|
case cancel
|
||||||
case proceed
|
case proceed
|
||||||
case invite(users: [String])
|
case invite(users: [String])
|
||||||
case toggleUser(UserProfileProxy)
|
case toggleUser(UserProfileProxy)
|
||||||
@@ -86,6 +87,7 @@ struct InviteUsersScreenViewStateBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenViewAction {
|
enum InviteUsersScreenViewAction {
|
||||||
|
case cancel
|
||||||
case proceed
|
case proceed
|
||||||
case toggleUser(UserProfileProxy)
|
case toggleUser(UserProfileProxy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
|
|
||||||
override func process(viewAction: InviteUsersScreenViewAction) {
|
override func process(viewAction: InviteUsersScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
|
case .cancel:
|
||||||
|
actionsSubject.send(.cancel)
|
||||||
case .proceed:
|
case .proceed:
|
||||||
switch roomType {
|
switch roomType {
|
||||||
case .draft:
|
case .draft:
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ struct InviteUsersScreen: View {
|
|||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
.navigationTitle(L10n.screenCreateRoomAddPeopleTitle)
|
.navigationTitle(L10n.screenCreateRoomAddPeopleTitle)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar { toolbar }
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
nextButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disableInteractiveDismissOnSearch()
|
.disableInteractiveDismissOnSearch()
|
||||||
.dismissSearchOnDisappear()
|
.dismissSearchOnDisappear()
|
||||||
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: L10n.commonSearchForSomeone)
|
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: L10n.commonSearchForSomeone)
|
||||||
@@ -123,11 +119,22 @@ struct InviteUsersScreen: View {
|
|||||||
.frame(width: frame.width)
|
.frame(width: frame.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var nextButton: some View {
|
@ToolbarContentBuilder
|
||||||
Button { context.send(viewAction: .proceed) } label: {
|
private var toolbar: some ToolbarContent {
|
||||||
Text(context.viewState.actionText)
|
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) {
|
private func deselect(_ user: UserProfileProxy) {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ struct InvitesScreenCell: View {
|
|||||||
|
|
||||||
private var separator: some View {
|
private var separator: some View {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.compound._borderRowSeparator)
|
.fill(Color.compound.borderDisabled)
|
||||||
.frame(height: 1 / UIScreen.main.scale)
|
.frame(height: 1 / UIScreen.main.scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,10 +91,9 @@ struct RoomDetailsEditScreen: View {
|
|||||||
prompt: canEditName ? Text(L10n.commonRoomNamePlaceholder) : nil,
|
prompt: canEditName ? Text(L10n.commonRoomNamePlaceholder) : nil,
|
||||||
axis: .horizontal)
|
axis: .horizontal)
|
||||||
.focused($focus, equals: .name)
|
.focused($focus, equals: .name)
|
||||||
.font(.compound.bodyLG)
|
.textFieldStyle(.compoundForm)
|
||||||
.foregroundColor(.compound.textPrimary)
|
|
||||||
.disabled(!canEditName)
|
.disabled(!canEditName)
|
||||||
.listRowBackground(canEditName ? nil : Color.clear)
|
.listRowBackground(canEditName ? Color.element.formRowBackground : .clear)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.commonRoomName)
|
Text(L10n.commonRoomName)
|
||||||
@@ -109,14 +108,13 @@ struct RoomDetailsEditScreen: View {
|
|||||||
|
|
||||||
TextField(L10n.commonTopic,
|
TextField(L10n.commonTopic,
|
||||||
text: $context.topic,
|
text: $context.topic,
|
||||||
prompt: canEditTopic ? Text(L10n.commonTopicPlaceholder) : nil,
|
prompt: canEditTopic ? Text(L10n.commonTopicPlaceholder).foregroundColor(.compound.textPlaceholder) : nil,
|
||||||
axis: .vertical)
|
axis: .vertical)
|
||||||
.focused($focus, equals: .topic)
|
.focused($focus, equals: .topic)
|
||||||
.font(.compound.bodyLG)
|
.textFieldStyle(.compoundForm)
|
||||||
.foregroundColor(.compound.textPrimary)
|
|
||||||
.disabled(!canEditTopic)
|
.disabled(!canEditTopic)
|
||||||
.listRowBackground(canEditTopic ? nil : Color.clear)
|
.listRowBackground(canEditTopic ? Color.element.formRowBackground : .clear)
|
||||||
.lineLimit(3, reservesSpace: true)
|
.lineLimit(3...)
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.commonTopic)
|
Text(L10n.commonTopic)
|
||||||
.formSectionHeader()
|
.formSectionHeader()
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
|
case .cancel:
|
||||||
|
navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
case .proceed:
|
case .proceed:
|
||||||
break
|
break
|
||||||
case .invite(let users):
|
case .invite(let users):
|
||||||
@@ -114,7 +116,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) { [weak self] in
|
navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) { [weak self] in
|
||||||
self?.selectedUsers.value = []
|
self?.selectedUsers.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import SwiftUI
|
|||||||
struct RoomDetailsScreen: View {
|
struct RoomDetailsScreen: View {
|
||||||
@ObservedObject var context: RoomDetailsScreenViewModel.Context
|
@ObservedObject var context: RoomDetailsScreenViewModel.Context
|
||||||
|
|
||||||
|
@State private var isTopicExpanded = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
if let recipient = context.viewState.dmRecipient {
|
if let recipient = context.viewState.dmRecipient {
|
||||||
@@ -111,8 +113,9 @@ struct RoomDetailsScreen: View {
|
|||||||
if let topic = context.viewState.topic, !topic.isEmpty {
|
if let topic = context.viewState.topic, !topic.isEmpty {
|
||||||
Text(topic)
|
Text(topic)
|
||||||
.foregroundColor(.compound.textSecondary)
|
.foregroundColor(.compound.textSecondary)
|
||||||
.font(.compound.bodySM)
|
.font(.compound.bodyMD)
|
||||||
.lineLimit(3)
|
.lineLimit(isTopicExpanded ? nil : 3)
|
||||||
|
.onTapGesture { isTopicExpanded.toggle() }
|
||||||
} else {
|
} else {
|
||||||
Button {
|
Button {
|
||||||
context.send(viewAction: .processTapAddTopic)
|
context.send(viewAction: .processTapAddTopic)
|
||||||
@@ -155,7 +158,7 @@ struct RoomDetailsScreen: View {
|
|||||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
|
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowSeparatorTint(.compound._borderRowSeparator)
|
.listRowSeparatorTint(.compound.borderDisabled)
|
||||||
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
||||||
.formSectionStyle()
|
.formSectionStyle()
|
||||||
.foregroundColor(.compound.textPrimary)
|
.foregroundColor(.compound.textPrimary)
|
||||||
@@ -247,7 +250,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
|
|||||||
.mockCharlie
|
.mockCharlie
|
||||||
]
|
]
|
||||||
let roomProxy = RoomProxyMock(with: .init(displayName: "Room A",
|
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,
|
isDirect: false,
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
canonicalAlias: "#alias:domain.com",
|
canonicalAlias: "#alias:domain.com",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var showLoading = false
|
var showLoading = false
|
||||||
var timelineStyle: TimelineStyle
|
var timelineStyle: TimelineStyle
|
||||||
var readReceiptsEnabled: Bool
|
var readReceiptsEnabled: Bool
|
||||||
|
var isEncryptedOneToOneRoom = false
|
||||||
|
|
||||||
var bindings: RoomScreenViewStateBindings
|
var bindings: RoomScreenViewStateBindings
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
roomAvatarURL: roomProxy.avatarURL,
|
roomAvatarURL: roomProxy.avatarURL,
|
||||||
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
|
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
|
||||||
readReceiptsEnabled: ServiceLocator.shared.settings.readReceiptsEnabled,
|
readReceiptsEnabled: ServiceLocator.shared.settings.readReceiptsEnabled,
|
||||||
|
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
|
||||||
bindings: .init(composerText: "", composerFocused: false)),
|
bindings: .init(composerText: "", composerFocused: false)),
|
||||||
imageProvider: mediaProvider)
|
imageProvider: mediaProvider)
|
||||||
|
|
||||||
|
|||||||
@@ -101,29 +101,41 @@ struct TimelineReplyView: View {
|
|||||||
|
|
||||||
var icon: Icon?
|
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 {
|
var body: some View {
|
||||||
HStack {
|
HStack(spacing: 8) {
|
||||||
iconView
|
iconView
|
||||||
.frame(width: imageContainerSize, height: imageContainerSize)
|
.frame(width: imageContainerSize, height: imageContainerSize)
|
||||||
.foregroundColor(.compound.iconPrimary)
|
.foregroundColor(.compound.iconPrimary)
|
||||||
.background(Color.compound.bgSubtlePrimary)
|
.background(Color.compound.bgSubtlePrimary)
|
||||||
.cornerRadius(icon?.cornerRadii ?? 0.0, corners: .allCorners)
|
.cornerRadius(icon?.cornerRadii ?? 0.0, corners: .allCorners)
|
||||||
|
|
||||||
if icon?.mediaSource == nil, icon?.systemIconName == nil {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Spacer().frame(width: 4.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(sender.displayName ?? sender.id)
|
Text(sender.displayName ?? sender.id)
|
||||||
.font(.compound.bodySMSemibold)
|
.font(.compound.bodySMSemibold)
|
||||||
.foregroundColor(.compound.textPrimary)
|
.foregroundColor(.compound.textPrimary)
|
||||||
|
|
||||||
Text(formattedBody ?? AttributedString(plainBody))
|
Text(messagePreview)
|
||||||
.font(.compound.bodyMD)
|
.font(.compound.bodyMD)
|
||||||
.foregroundColor(.compound.textPlaceholder)
|
.foregroundColor(.compound.textSecondary)
|
||||||
.tint(.compound.textLinkExternal)
|
.tint(.compound.textLinkExternal)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
}
|
}
|
||||||
|
.padding(.leading, isTextOnly ? 8 : 0)
|
||||||
|
.padding(.trailing, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +166,7 @@ struct TimelineReplyView_Previews: PreviewProvider {
|
|||||||
static let viewModel = RoomScreenViewModel.mock
|
static let viewModel = RoomScreenViewModel.mock
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: ""))
|
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: ""))
|
||||||
|
|
||||||
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loading(eventID: ""))
|
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loading(eventID: ""))
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ struct RoomAttachmentPicker: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
|
||||||
.presentationDetents([.height(sheetContentHeight)])
|
.presentationDetents([.height(sheetContentHeight)])
|
||||||
|
.presentationBackground(Color.compound.bgCanvasDefault)
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
|
|
||||||
@State private var showItemActionMenu = false
|
@State private var showItemActionMenu = false
|
||||||
|
|
||||||
private var isTextItem: Bool {
|
private var isTextItem: Bool { timelineItem is TextBasedRoomTimelineItem }
|
||||||
timelineItem is TextBasedRoomTimelineItem
|
private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom }
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .trailingFirstTextBaseline) {
|
ZStack(alignment: .trailingFirstTextBaseline) {
|
||||||
VStack(alignment: alignment, spacing: -12) {
|
VStack(alignment: alignment, spacing: -12) {
|
||||||
if !timelineItem.isOutgoing {
|
if !timelineItem.isOutgoing, !isEncryptedOneToOneRoom {
|
||||||
header
|
header
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
}
|
}
|
||||||
@@ -213,7 +212,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
// The rendered reply bubble with a greedy width. The custom layout prevents
|
// The rendered reply bubble with a greedy width. The custom layout prevents
|
||||||
// the infinite width from increasing the overall width of the view.
|
// the infinite width from increasing the overall width of the view.
|
||||||
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
||||||
.foregroundColor(.compound.textPlaceholder)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding(4.0)
|
.padding(4.0)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -235,7 +233,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var messageBubbleTopPadding: CGFloat {
|
private var messageBubbleTopPadding: CGFloat {
|
||||||
guard timelineItem.isOutgoing else { return 0 }
|
guard timelineItem.isOutgoing || isEncryptedOneToOneRoom else { return 0 }
|
||||||
return timelineGroupStyle == .single || timelineGroupStyle == .first ? 8 : 0
|
return timelineGroupStyle == .single || timelineGroupStyle == .first ? 8 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,13 +296,14 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var mockTimeline: some View {
|
static var mockTimeline: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
ScrollView {
|
||||||
ForEach(viewModel.state.items) { item in
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
item.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
|
ForEach(viewModel.state.items) { item in
|
||||||
|
item.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environment(\.timelineStyle, .bubbles)
|
.environment(\.timelineStyle, .bubbles)
|
||||||
.previewLayout(.sizeThatFits)
|
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ struct TimelineReactionButton: View {
|
|||||||
.background(
|
.background(
|
||||||
backgroundShape
|
backgroundShape
|
||||||
.strokeBorder(reaction.isHighlighted ? Color.compound.textSecondary : .compound.bgCanvasDefault, lineWidth: 2)
|
.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)
|
.accessibilityElement(children: .combine)
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-22
@@ -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 {
|
private struct CollapsibleRoomTimelineItemDisclosureGroupStyle: DisclosureGroupStyle {
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
VStack(spacing: 0.0) {
|
VStack(spacing: 0.0) {
|
||||||
HStack(alignment: .center) {
|
Button { configuration.isExpanded.toggle() } label: {
|
||||||
configuration.label
|
HStack(alignment: .center) {
|
||||||
Text(Image(systemName: "chevron.forward"))
|
configuration.label
|
||||||
.rotationEffect(.degrees(configuration.isExpanded ? 90 : 0))
|
Text(Image(systemName: "chevron.forward"))
|
||||||
.animation(.elementDefault, value: configuration.isExpanded)
|
.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)
|
.frame(maxWidth: .infinity)
|
||||||
.font(.compound.bodySM)
|
.padding(.top, 8.0)
|
||||||
.foregroundColor(.compound.textSecondary)
|
|
||||||
.padding(.horizontal, 36.0)
|
|
||||||
.padding(.top, 20.0)
|
|
||||||
.padding(.bottom, 12.0)
|
|
||||||
|
|
||||||
.onTapGesture {
|
|
||||||
configuration.isExpanded.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
if configuration.isExpanded {
|
if configuration.isExpanded {
|
||||||
configuration.content
|
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
|
// The rendered blockquote with a greedy width. The custom layout prevents the
|
||||||
// infinite width from increasing the overall width of the view.
|
// infinite width from increasing the overall width of the view.
|
||||||
Text(component.attributedString.mergingAttributes(blockquoteAttributes))
|
Text(component.attributedString.mergingAttributes(blockquoteAttributes))
|
||||||
.foregroundColor(.compound.textPlaceholder)
|
.foregroundColor(.compound.textSecondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.leading, 12.0)
|
.padding(.leading, 12.0)
|
||||||
.overlay(alignment: .leading) {
|
.overlay(alignment: .leading) {
|
||||||
// User an overlay here so that the rectangle's infinite height doesn't take priority
|
// User an overlay here so that the rectangle's infinite height doesn't take priority
|
||||||
Rectangle()
|
Capsule()
|
||||||
.frame(width: 2.0)
|
.frame(width: 2.0)
|
||||||
.padding(.leading, 6.0)
|
.padding(.leading, 5.0)
|
||||||
.foregroundColor(.compound.textPlaceholder)
|
.foregroundColor(.compound.textSecondary)
|
||||||
|
.padding(.vertical, 2)
|
||||||
}
|
}
|
||||||
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
|
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
|
||||||
} else {
|
} 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>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>
|
<code>Hello world</code>
|
||||||
<p>Text</p>
|
<p>Text</p>
|
||||||
<code><b>Hello</b> <i>world</i></code>
|
<code><b>Hello</b> <i>world</i></code>
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ public struct TimelineItemMenu: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
|
.presentationBackground(Color.compound.bgCanvasDefault)
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ public struct TimelineItemMenu: View {
|
|||||||
|
|
||||||
private func reactionBackgroundColor(for emoji: String) -> Color {
|
private func reactionBackgroundColor(for emoji: String) -> Color {
|
||||||
if item.properties.reactions.first(where: { $0.key == emoji }) != nil {
|
if item.properties.reactions.first(where: { $0.key == emoji }) != nil {
|
||||||
return .compound._bgReactionButton
|
return .compound.bgActionPrimaryRest
|
||||||
} else {
|
} else {
|
||||||
return .clear
|
return .clear
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
|
case .cancel:
|
||||||
|
break // Not shown in this flow.
|
||||||
case .proceed:
|
case .proceed:
|
||||||
openCreateRoomScreen()
|
openCreateRoomScreen()
|
||||||
case .invite:
|
case .invite:
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ struct StartChatScreen: View {
|
|||||||
private var inviteFriendsSection: some View {
|
private var inviteFriendsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
MatrixUserShareLink(userID: context.viewState.userID) {
|
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())
|
.buttonStyle(FormButtonStyle())
|
||||||
.accessibilityIdentifier(A11yIdentifiers.startChatScreen.inviteFriends)
|
.accessibilityIdentifier(A11yIdentifiers.startChatScreen.inviteFriends)
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ packages:
|
|||||||
minorVersion: 5.13.0
|
minorVersion: 5.13.0
|
||||||
Compound:
|
Compound:
|
||||||
url: https://github.com/vector-im/compound-ios
|
url: https://github.com/vector-im/compound-ios
|
||||||
revision: d59c317362beba940baa43d6aacdd357e208048d
|
revision: e8b35fdd8c4008079dfce203e63bf7a05582d7b9
|
||||||
# path: ../compound-ios
|
# path: ../compound-ios
|
||||||
Algorithms:
|
Algorithms:
|
||||||
url: https://github.com/apple/swift-algorithms
|
url: https://github.com/apple/swift-algorithms
|
||||||
|
|||||||
Reference in New Issue
Block a user