mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Initial timeline design tweaks (#505)
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
"action_confirm" = "Confirm";
|
||||
"action_match" = "Match";
|
||||
|
||||
"new_message" = "New message";
|
||||
|
||||
"screenshot_detected_title" = "You took a screenshot";
|
||||
"screenshot_detected_message" = "Would you like to submit a bug report?";
|
||||
|
||||
@@ -20,6 +22,7 @@
|
||||
"room_timeline_replying_to" = "Replying to %@";
|
||||
"room_timeline_editing" = "Editing";
|
||||
"room_timeline_syncing" = "Syncing";
|
||||
"room_timeline_unable_to_decrypt" = "Unable to decrypt";
|
||||
|
||||
"room_timeline_context_menu_retry_decryption" = "Retry decryption";
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ extension ElementL10n {
|
||||
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
||||
/// Tablet
|
||||
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
|
||||
/// New message
|
||||
public static let newMessage = ElementL10n.tr("Untranslated", "new_message")
|
||||
/// %1$@ accepted the invite
|
||||
public static func noticeRoomInviteAccepted(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "noticeRoomInviteAccepted", String(describing: p1))
|
||||
@@ -110,6 +112,8 @@ extension ElementL10n {
|
||||
public static let roomTimelineStylePlainLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_plain_long_description")
|
||||
/// Syncing
|
||||
public static let roomTimelineSyncing = ElementL10n.tr("Untranslated", "room_timeline_syncing")
|
||||
/// Unable to decrypt
|
||||
public static let roomTimelineUnableToDecrypt = ElementL10n.tr("Untranslated", "room_timeline_unable_to_decrypt")
|
||||
/// Would you like to submit a bug report?
|
||||
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
|
||||
/// You took a screenshot
|
||||
|
||||
@@ -70,7 +70,6 @@ struct HomeScreenRoomCell: View {
|
||||
Text(timestamp)
|
||||
.font(.element.footnote)
|
||||
.foregroundColor(room.hasUnreads ? .element.brand : .element.tertiaryContent)
|
||||
.id(timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +84,9 @@ struct HomeScreenRoomCell: View {
|
||||
if let lastMessage = room.lastMessage, !String(lastMessage.characters).isEmpty {
|
||||
Text(lastMessage)
|
||||
.lastMessageFormatting()
|
||||
.id(lastMessage)
|
||||
} else {
|
||||
Text(ElementL10n.newMessage)
|
||||
.lastMessageFormatting()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,15 @@ enum RoomScreenComposerMode: Equatable {
|
||||
case `default`
|
||||
case reply(id: String, displayName: String)
|
||||
case edit(originalItemId: String)
|
||||
|
||||
var isEdit: Bool {
|
||||
switch self {
|
||||
case .edit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomScreenViewAction {
|
||||
|
||||
@@ -211,7 +211,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
var actions: [TimelineItemContextMenuAction] = [
|
||||
.react, .copy, .quote, .copyPermalink, .reply
|
||||
.react, .copy, .reply
|
||||
// Disabled for FOSDEM
|
||||
// .quote, .copyPermalink
|
||||
]
|
||||
|
||||
if item.isEditable {
|
||||
@@ -280,3 +282,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mocks
|
||||
|
||||
extension RoomScreenViewModel {
|
||||
static let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
roomName: "Preview room")
|
||||
}
|
||||
|
||||
@@ -27,38 +27,48 @@ struct MessageComposer: View {
|
||||
let editCancellationAction: () -> Void
|
||||
|
||||
var body: some View {
|
||||
let rect = RoundedRectangle(cornerRadius: borderRadius)
|
||||
VStack(alignment: .leading, spacing: 4.0) {
|
||||
let roundedRectangle = RoundedRectangle(cornerRadius: borderRadius)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
header
|
||||
HStack(alignment: .center) {
|
||||
HStack(alignment: .bottom) {
|
||||
MessageComposerTextField(placeholder: ElementL10n.roomMessagePlaceholder,
|
||||
text: $text,
|
||||
focused: $focused,
|
||||
maxHeight: 300,
|
||||
onEnterKeyHandler: sendAction)
|
||||
.tint(.element.brand)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
Button {
|
||||
sendAction()
|
||||
} label: {
|
||||
Image(systemName: "paperplane")
|
||||
.font(.element.title3)
|
||||
.foregroundColor(sendingDisabled ? .element.tempActionBackground : .element.tempActionForeground)
|
||||
.padding(8.0)
|
||||
.background(
|
||||
submitButtonImage
|
||||
.symbolVariant(.fill)
|
||||
.font(.element.body)
|
||||
.foregroundColor(sendingDisabled ? .element.quaternaryContent : .element.background)
|
||||
.padding(5)
|
||||
.background {
|
||||
Circle()
|
||||
.foregroundColor(sendingDisabled ? .clear : .element.tempActionBackground)
|
||||
)
|
||||
.foregroundColor(sendingDisabled ? .clear : .element.brand)
|
||||
}
|
||||
}
|
||||
.disabled(sendingDisabled)
|
||||
.animation(.elementDefault, value: sendingDisabled)
|
||||
.keyboardShortcut(.return, modifiers: [.command])
|
||||
.padding(4.0)
|
||||
.padding(8)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 12.0)
|
||||
.background(.thinMaterial)
|
||||
.clipShape(rect)
|
||||
.background {
|
||||
ZStack {
|
||||
roundedRectangle
|
||||
.fill(Color.element.system)
|
||||
roundedRectangle
|
||||
.stroke(Color.element.quinaryContent, lineWidth: 1)
|
||||
.opacity(focused ? 1 : 0)
|
||||
}
|
||||
}
|
||||
.clipShape(roundedRectangle)
|
||||
.animation(.elementDefault, value: type)
|
||||
}
|
||||
|
||||
@@ -74,6 +84,17 @@ struct MessageComposer: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var submitButtonImage: some View {
|
||||
// ZStack with opacity so the button size is consistent.
|
||||
ZStack {
|
||||
Image(systemName: "checkmark")
|
||||
.opacity(type.isEdit ? 1 : 0)
|
||||
.fontWeight(.medium)
|
||||
Image(systemName: "paperplane")
|
||||
.opacity(type.isEdit ? 0 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
private var borderRadius: CGFloat {
|
||||
switch type {
|
||||
case .default:
|
||||
@@ -90,18 +111,18 @@ private struct MessageComposerReplyHeader: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Label(ElementL10n.roomTimelineReplyingTo(displayName), systemImage: "arrow.uturn.left")
|
||||
.font(.element.caption2)
|
||||
Label(ElementL10n.roomTimelineReplyingTo(displayName), systemImage: "arrowshape.turn.up.left")
|
||||
.font(.element.caption1)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Button {
|
||||
action()
|
||||
} label: {
|
||||
Image(systemName: "x.circle")
|
||||
.font(.element.callout)
|
||||
Image(systemName: "xmark")
|
||||
.font(.element.caption2.weight(.medium))
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(4.0)
|
||||
.padding(12.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,18 +133,18 @@ private struct MessageComposerEditHeader: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Label(ElementL10n.roomTimelineEditing, systemImage: "pencil")
|
||||
.font(.element.caption2)
|
||||
Label(ElementL10n.roomTimelineEditing, systemImage: "pencil.line")
|
||||
.font(.element.caption1)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Button {
|
||||
action()
|
||||
} label: {
|
||||
Image(systemName: "x.circle")
|
||||
.font(.element.callout)
|
||||
Image(systemName: "xmark")
|
||||
.font(.element.caption2.weight(.medium))
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(4.0)
|
||||
.padding(12.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +161,14 @@ struct MessageComposer_Previews: PreviewProvider {
|
||||
replyCancellationAction: { },
|
||||
editCancellationAction: { })
|
||||
|
||||
MessageComposer(text: .constant("This is a short message."),
|
||||
focused: .constant(false),
|
||||
sendingDisabled: false,
|
||||
type: .default,
|
||||
sendAction: { },
|
||||
replyCancellationAction: { },
|
||||
editCancellationAction: { })
|
||||
|
||||
MessageComposer(text: .constant("This is a very long message that will wrap to 2 lines on an iPhone 14."),
|
||||
focused: .constant(false),
|
||||
sendingDisabled: false,
|
||||
|
||||
@@ -22,8 +22,6 @@ struct MessageComposerTextField: View {
|
||||
@Binding private var text: String
|
||||
@Binding private var focused: Bool
|
||||
|
||||
@State private var dynamicHeight: CGFloat = 100
|
||||
|
||||
private let placeholder: String
|
||||
private let maxHeight: CGFloat
|
||||
private let onEnterKeyHandler: OnEnterKeyHandler
|
||||
@@ -50,11 +48,9 @@ struct MessageComposerTextField: View {
|
||||
|
||||
var body: some View {
|
||||
UITextViewWrapper(text: $text,
|
||||
calculatedHeight: $dynamicHeight,
|
||||
focused: $focused,
|
||||
maxHeight: maxHeight,
|
||||
onEnterKeyHandler: onEnterKeyHandler)
|
||||
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
|
||||
.background(placeholderView, alignment: .topLeading)
|
||||
}
|
||||
|
||||
@@ -71,7 +67,6 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
typealias UIViewType = UITextView
|
||||
|
||||
@Binding var text: String
|
||||
@Binding var calculatedHeight: CGFloat
|
||||
@Binding var focused: Bool
|
||||
|
||||
let maxHeight: CGFloat
|
||||
@@ -84,7 +79,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
textView.keyDelegate = context.coordinator
|
||||
textView.textColor = .element.primaryContent
|
||||
textView.isEditable = true
|
||||
textView.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
textView.font = .preferredFont(forTextStyle: .body)
|
||||
textView.isSelectable = true
|
||||
textView.isUserInteractionEnabled = true
|
||||
textView.backgroundColor = UIColor.clear
|
||||
@@ -97,6 +92,15 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
|
||||
// Note: Coalescing a width of zero here returns a size for the view with 1 line of text visible.
|
||||
let newSize = uiView.sizeThatFits(CGSize(width: proposal.width ?? .zero,
|
||||
height: CGFloat.greatestFiniteMagnitude))
|
||||
let width = proposal.width ?? newSize.width
|
||||
let height = min(maxHeight, newSize.height)
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
|
||||
if textView.text != text {
|
||||
@@ -114,8 +118,6 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
UITextViewWrapper.recalculateHeight(view: textView, result: $calculatedHeight, maxHeight: maxHeight)
|
||||
|
||||
DispatchQueue.main.async { // Avoid cycle detected through attribute warnings
|
||||
if focused, textView.window != nil, !textView.isFirstResponder {
|
||||
textView.becomeFirstResponder()
|
||||
@@ -125,36 +127,21 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(text: $text,
|
||||
height: $calculatedHeight,
|
||||
focused: $focused,
|
||||
maxHeight: maxHeight,
|
||||
onEnterKeyHandler: onEnterKeyHandler)
|
||||
}
|
||||
|
||||
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>, maxHeight: CGFloat) {
|
||||
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
let height = min(maxHeight, newSize.height)
|
||||
|
||||
if result.wrappedValue != height {
|
||||
DispatchQueue.main.async {
|
||||
result.wrappedValue = height // Must be called asynchronously
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate, TextViewWithKeyDetectionDelegate {
|
||||
private var text: Binding<String>
|
||||
private var calculatedHeight: Binding<CGFloat>
|
||||
private var focused: Binding<Bool>
|
||||
|
||||
private let maxHeight: CGFloat
|
||||
|
||||
private let onEnterKeyHandler: OnEnterKeyHandler
|
||||
|
||||
init(text: Binding<String>, height: Binding<CGFloat>, focused: Binding<Bool>, maxHeight: CGFloat, onEnterKeyHandler: @escaping OnEnterKeyHandler) {
|
||||
init(text: Binding<String>, focused: Binding<Bool>, maxHeight: CGFloat, onEnterKeyHandler: @escaping OnEnterKeyHandler) {
|
||||
self.text = text
|
||||
calculatedHeight = height
|
||||
self.focused = focused
|
||||
self.maxHeight = maxHeight
|
||||
self.onEnterKeyHandler = onEnterKeyHandler
|
||||
@@ -162,9 +149,6 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
text.wrappedValue = textView.text
|
||||
UITextViewWrapper.recalculateHeight(view: textView,
|
||||
result: calculatedHeight,
|
||||
maxHeight: maxHeight)
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
@@ -212,6 +196,7 @@ struct MessageComposerTextField_Previews: PreviewProvider {
|
||||
VStack {
|
||||
PreviewWrapper(text: "123")
|
||||
PreviewWrapper(text: "")
|
||||
PreviewWrapper(text: "A really long message that will wrap to multiple lines on a phone in portrait.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,15 @@ struct RoomHeaderView: View {
|
||||
@ObservedObject var context: RoomScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 12) {
|
||||
roomAvatar
|
||||
.accessibilityHidden(true)
|
||||
Text(context.viewState.roomTitle)
|
||||
.font(.element.headline)
|
||||
.accessibilityIdentifier("roomNameLabel")
|
||||
}
|
||||
// Leading align whilst using the principal toolbar position.
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
// Using a button stops is from getting truncated in the navigation bar
|
||||
.onTapGesture {
|
||||
context.send(viewAction: .displayRoomDetails)
|
||||
|
||||
@@ -23,9 +23,12 @@ struct RoomScreen: View {
|
||||
|
||||
var body: some View {
|
||||
timeline
|
||||
.background(Color.element.background) // Kills the toolbar translucency.
|
||||
.safeAreaInset(edge: .bottom) { messageComposer }
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.toolbarRole(.editor) // Hide the back button title.
|
||||
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
|
||||
.overlay { loadingIndicator }
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
|
||||
@@ -89,14 +92,6 @@ struct RoomScreen: View {
|
||||
ToolbarItem(placement: .principal) {
|
||||
RoomHeaderView(context: context)
|
||||
}
|
||||
|
||||
ToolbarItem(id: "RoomDetailsAction", placement: .secondaryAction, showsByDefault: false) {
|
||||
Button {
|
||||
context.send(viewAction: .displayRoomDetails)
|
||||
} label: {
|
||||
Label("Room Details", systemImage: "info.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
||||
+41
-44
@@ -25,6 +25,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ScaledMetric private var senderNameVerticalPadding = 3
|
||||
private let cornerRadius: CGFloat = 12
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .trailingFirstTextBaseline) {
|
||||
@@ -39,7 +40,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
styledContentWithReactions
|
||||
messageBubbleWithReactions
|
||||
}
|
||||
.padding(.horizontal, 16.0)
|
||||
.padding(timelineItem.isOutgoing ? .leading : .trailing, 40) // Extra padding to differentiate alignment.
|
||||
@@ -71,11 +72,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var styledContentWithReactions: some View {
|
||||
private var messageBubbleWithReactions: some View {
|
||||
// Figma has a spacing of -4 but it doesn't take into account
|
||||
// the centre aligned stroke width so we use -5 here
|
||||
VStack(alignment: alignment, spacing: -5) {
|
||||
styledContent
|
||||
messageBubble
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
if !timelineItem.properties.reactions.isEmpty {
|
||||
@@ -86,24 +87,25 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var messageBubble: some View {
|
||||
styledContent
|
||||
.contentShape(.contextMenuPreview, RoundedCornerShape(radius: cornerRadius, corners: timelineItem.roundedCorners)) // Rounded corners for the context menu animation.
|
||||
.contextMenu { [weak context] in
|
||||
context?.viewState.contextMenuActionProvider?(timelineItem.id).map { actions in
|
||||
TimelineItemContextMenu(itemID: timelineItem.id, contextMenuActions: actions)
|
||||
}
|
||||
}
|
||||
.padding(.top, messageBubbleTopPadding)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var styledContent: some View {
|
||||
if timelineItem.isOutgoing {
|
||||
styledContentOutgoing
|
||||
} else {
|
||||
styledContentIncoming
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var styledContentOutgoing: some View {
|
||||
let topPadding: CGFloat? = timelineItem.groupState == .single || timelineItem.groupState == .beginning ? 8 : 0
|
||||
|
||||
if shouldAvoidBubbling {
|
||||
content()
|
||||
.cornerRadius(12, corners: timelineItem.roundedCorners)
|
||||
.padding(.top, topPadding)
|
||||
.bubbleStyle(inset: false,
|
||||
cornerRadius: cornerRadius,
|
||||
corners: timelineItem.roundedCorners)
|
||||
} else {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
content()
|
||||
@@ -114,36 +116,20 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||
.background(Color.element.systemGray5)
|
||||
.cornerRadius(12, corners: timelineItem.roundedCorners)
|
||||
.padding(.top, topPadding)
|
||||
.bubbleStyle(inset: true,
|
||||
color: timelineItem.isOutgoing ? .element.systemGray5 : .element.systemGray6,
|
||||
cornerRadius: cornerRadius,
|
||||
corners: timelineItem.roundedCorners)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var styledContentIncoming: some View {
|
||||
if shouldAvoidBubbling {
|
||||
content()
|
||||
.cornerRadius(12, corners: timelineItem.roundedCorners)
|
||||
} else {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
content()
|
||||
|
||||
if timelineItem.properties.isEdited {
|
||||
Text(ElementL10n.editedSuffix)
|
||||
.font(.element.caption2)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||
.background(Color.element.systemGray6)
|
||||
.cornerRadius(12, corners: timelineItem.roundedCorners)
|
||||
}
|
||||
|
||||
private var messageBubbleTopPadding: CGFloat {
|
||||
guard timelineItem.isOutgoing else { return 0 }
|
||||
return timelineItem.groupState == .single || timelineItem.groupState == .beginning ? 8 : 0
|
||||
}
|
||||
|
||||
private var shouldAvoidBubbling: Bool {
|
||||
timelineItem is ImageRoomTimelineItem || timelineItem is VideoRoomTimelineItem
|
||||
timelineItem is ImageRoomTimelineItem || timelineItem is VideoRoomTimelineItem || timelineItem is StickerRoomTimelineItem
|
||||
}
|
||||
|
||||
private var alignment: HorizontalAlignment {
|
||||
@@ -151,16 +137,27 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func bubbleStyle(inset: Bool, color: Color? = nil, cornerRadius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
padding(inset ? 8 : 0)
|
||||
.background(inset ? color : nil)
|
||||
.cornerRadius(cornerRadius, corners: corners)
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
|
||||
let item = MockRoomTimelineController().timelineItems[index]
|
||||
RoomTimelineViewFactory().buildTimelineViewFor(timelineItem: item)
|
||||
.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
|
||||
}
|
||||
}
|
||||
.timelineStyle(.bubbles)
|
||||
.padding(.horizontal, 8)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
header
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@@ -48,7 +48,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
HStack {
|
||||
TimelineSenderAvatarView(timelineItem: timelineItem)
|
||||
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
|
||||
.font(.body)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
@@ -57,6 +57,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
.foregroundColor(Color.element.tertiaryContent)
|
||||
.font(.element.caption2)
|
||||
}
|
||||
.padding(.top, 16)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,15 +81,18 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
}
|
||||
|
||||
struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
|
||||
let item = MockRoomTimelineController().timelineItems[index]
|
||||
RoomTimelineViewFactory().buildTimelineViewFor(timelineItem: item)
|
||||
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
|
||||
}
|
||||
}
|
||||
.timelineStyle(.plain)
|
||||
.padding(.horizontal, 8)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,16 +43,16 @@ struct TimelineReactionButton: View {
|
||||
var label: some View {
|
||||
HStack(spacing: 4) {
|
||||
Text(reaction.key)
|
||||
.font(.element.caption1)
|
||||
.font(.element.subheadline)
|
||||
Text(String(reaction.count))
|
||||
.font(.element.caption1)
|
||||
.font(.element.subheadline)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 8)
|
||||
.background(
|
||||
Capsule()
|
||||
.strokeBorder(reaction.isHighlighted ? Color.element.accent : .element.background, lineWidth: 2)
|
||||
.strokeBorder(reaction.isHighlighted ? Color.element.secondaryContent : .element.background, lineWidth: 2)
|
||||
.background(reaction.isHighlighted ? Color.element.accent.opacity(0.1) : .element.system, in: Capsule())
|
||||
)
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
@@ -32,9 +32,11 @@ struct EmoteRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -17,46 +17,36 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EncryptedRoomTimelineView: View {
|
||||
@State private var showEncryptionInfo = false
|
||||
|
||||
let timelineItem: EncryptedRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
Button {
|
||||
showEncryptionInfo = !showEncryptionInfo
|
||||
} label: {
|
||||
Label {
|
||||
if showEncryptionInfo {
|
||||
FormattedBodyText(text: encryptionDetails)
|
||||
} else {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "lock.shield")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.animation(nil, value: showEncryptionInfo)
|
||||
Label {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
} icon: {
|
||||
Image(systemName: "lock.shield")
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
}
|
||||
.labelStyle(RoomTimelineViewLabelStyle())
|
||||
}
|
||||
}
|
||||
|
||||
private var encryptionDetails: String {
|
||||
switch timelineItem.encryptionType {
|
||||
case .unknown:
|
||||
return "Unknown"
|
||||
case .megolmV1AesSha2(let sessionId):
|
||||
return "Megolm session id: \(sessionId)"
|
||||
case .olmV1Curve25519AesSha2(let senderKey):
|
||||
return "Olm sender key: \(senderKey)"
|
||||
}
|
||||
|
||||
struct RoomTimelineViewLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
configuration.icon
|
||||
configuration.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EncryptedRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -34,9 +34,11 @@ struct FileRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct FileRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -38,14 +38,16 @@ struct FormattedBodyText: View {
|
||||
} else {
|
||||
Text(component.attributedString.mergingAttributes(blockquoteAttributes))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
.lineLimit(3) // FIXME: Quotes vs replies
|
||||
.padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12))
|
||||
.clipped()
|
||||
.background(Color.element.systemGray4)
|
||||
.cornerRadius(13)
|
||||
.background(Color.element.background)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
} else {
|
||||
Text(component.attributedString)
|
||||
.padding(.horizontal, timelineStyle == .bubbles ? 4 : 0) // FIXME: Configurable
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
@@ -56,7 +58,7 @@ struct FormattedBodyText: View {
|
||||
|
||||
private var blockquoteAttributes: AttributeContainer {
|
||||
var container = AttributeContainer()
|
||||
container.font = .element.caption1
|
||||
container.font = .element.subheadline
|
||||
return container
|
||||
}
|
||||
}
|
||||
@@ -70,7 +72,12 @@ extension FormattedBodyText {
|
||||
struct FormattedBodyText_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
.padding(8)
|
||||
.background(Color.element.systemGray6)
|
||||
.cornerRadius(12)
|
||||
|
||||
body
|
||||
.timelineStyle(.plain)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -80,8 +87,7 @@ struct FormattedBodyText_Previews: PreviewProvider {
|
||||
Text before blockquote
|
||||
<blockquote>
|
||||
<b>bold</b> <i>italic</i>
|
||||
</blockquote>
|
||||
Text after blockquote
|
||||
</blockquote>Text after blockquote
|
||||
""",
|
||||
"""
|
||||
<blockquote>First blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>
|
||||
|
||||
@@ -28,6 +28,7 @@ struct ImageRoomTimelineView: View {
|
||||
imageProvider: context.imageProvider) {
|
||||
placeholder
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
@@ -45,9 +46,11 @@ struct ImageRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -22,7 +22,7 @@ struct NoticeRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
HStack(alignment: .top) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Image(systemName: "exclamationmark.bubble").padding(.top, 2.0)
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
@@ -35,9 +35,11 @@ struct NoticeRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -31,12 +31,15 @@ struct RedactedRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct RedactedRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading, spacing: 20.0) {
|
||||
RedactedRoomTimelineView(timelineItem: itemWith(text: ElementL10n.eventRedacted,
|
||||
timestamp: "Later",
|
||||
senderId: "Anne"))
|
||||
}
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
|
||||
|
||||
@@ -23,7 +23,8 @@ struct SeparatorRoomTimelineView: View {
|
||||
Text(timelineItem.text)
|
||||
.font(.element.footnote)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(.vertical, 24)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct StateRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
Text(timelineItem.text)
|
||||
.font(.element.caption1Bold)
|
||||
.font(.element.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
@@ -37,7 +37,14 @@ struct StateRoomTimelineView_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
let item = StateRoomTimelineItem(id: UUID().uuidString, text: "Alice joined", timestamp: "Now", groupState: .beginning, isOutgoing: false, isEditable: false, sender: .init(id: ""))
|
||||
return StateRoomTimelineView(timelineItem: item)
|
||||
StateRoomTimelineView(timelineItem: item)
|
||||
}
|
||||
|
||||
static let item = StateRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Alice joined",
|
||||
timestamp: "Now",
|
||||
groupState: .beginning,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: ""))
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ struct StickerRoomTimelineView: View {
|
||||
imageProvider: context.imageProvider) {
|
||||
placeholder
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.accessibilityLabel(timelineItem.text)
|
||||
@@ -46,9 +47,11 @@ struct StickerRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -32,9 +32,11 @@ struct TextRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct TextRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -34,16 +34,18 @@ struct UnsupportedRoomTimelineView: View {
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.foregroundColor(.red)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -31,6 +31,7 @@ struct VideoRoomTimelineView: View {
|
||||
} placeholder: {
|
||||
placeholder
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
@@ -56,9 +57,11 @@ struct VideoRoomTimelineView: View {
|
||||
}
|
||||
|
||||
struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
body
|
||||
body.timelineStyle(.plain)
|
||||
body.environmentObject(viewModel.context)
|
||||
body.timelineStyle(.plain).environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
|
||||
@@ -54,8 +54,10 @@ enum TimelineItemContextMenuAction: Identifiable, Hashable {
|
||||
}
|
||||
|
||||
public struct TimelineItemContextMenu: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
|
||||
let itemID: String
|
||||
let contextMenuActions: TimelineItemContextMenuActions
|
||||
let callback: (TimelineItemContextMenuAction) -> Void
|
||||
|
||||
public var body: some View {
|
||||
viewsForActions(contextMenuActions.actions)
|
||||
@@ -67,45 +69,49 @@ public struct TimelineItemContextMenu: View {
|
||||
}
|
||||
|
||||
private func viewsForActions(_ actions: [TimelineItemContextMenuAction]) -> some View {
|
||||
ForEach(actions, id: \.self) { item in
|
||||
switch item {
|
||||
ForEach(actions, id: \.self) { action in
|
||||
switch action {
|
||||
case .react:
|
||||
Button { callback(item) } label: {
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.reactions, systemImage: "face.smiling")
|
||||
}
|
||||
case .copy:
|
||||
Button { callback(item) } label: {
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.actionCopy, systemImage: "doc.on.doc")
|
||||
}
|
||||
case .edit:
|
||||
Button { callback(item) } label: {
|
||||
Label(ElementL10n.edit, systemImage: "pencil")
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.edit, systemImage: "pencil.line")
|
||||
}
|
||||
case .quote:
|
||||
Button { callback(item) } label: {
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.actionQuote, systemImage: "quote.bubble")
|
||||
}
|
||||
case .copyPermalink:
|
||||
Button { callback(item) } label: {
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.permalink, systemImage: "link")
|
||||
}
|
||||
case .reply:
|
||||
Button { callback(item) } label: {
|
||||
Label(ElementL10n.reply, systemImage: "arrow.uturn.left")
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.reply, systemImage: "arrowshape.turn.up.left")
|
||||
}
|
||||
case .redact:
|
||||
Button(role: .destructive) { callback(item) } label: {
|
||||
Button(role: .destructive) { send(action) } label: {
|
||||
Label(ElementL10n.messageActionItemRedact, systemImage: "trash")
|
||||
}
|
||||
case .viewSource:
|
||||
Button { callback(item) } label: {
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.viewSource, systemImage: "doc.text.below.ecg")
|
||||
}
|
||||
case .retryDecryption:
|
||||
Button { callback(item) } label: {
|
||||
Button { send(action) } label: {
|
||||
Label(ElementL10n.roomTimelineContextMenuRetryDecryption, systemImage: "arrow.down.message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func send(_ action: TimelineItemContextMenuAction) {
|
||||
context.send(viewAction: .contextMenuAction(itemID: itemID, action: action))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ class TimelineTableViewController: UIViewController {
|
||||
tableView.separatorStyle = .none
|
||||
tableView.allowsSelection = false
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.backgroundColor = .element.background
|
||||
view.addSubview(tableView)
|
||||
|
||||
scrollToBottomPublisher
|
||||
@@ -189,7 +190,6 @@ class TimelineTableViewController: UIViewController {
|
||||
// A local reference to avoid capturing self in the cell configuration.
|
||||
let coordinator = self.coordinator
|
||||
let opacity = self.opacity(for: timelineItem)
|
||||
let contextMenuActionProvider = self.contextMenuActionProvider
|
||||
|
||||
cell.item = timelineItem
|
||||
cell.contentConfiguration = UIHostingConfiguration {
|
||||
@@ -197,13 +197,6 @@ class TimelineTableViewController: UIViewController {
|
||||
.id(timelineItem.id)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.opacity(opacity)
|
||||
.contextMenu {
|
||||
contextMenuActionProvider?(timelineItem.id).map { actions in
|
||||
TimelineItemContextMenu(contextMenuActions: actions) { action in
|
||||
coordinator.send(viewAction: .contextMenuAction(itemID: timelineItem.id, action: action))
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
coordinator.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
@@ -223,6 +216,7 @@ class TimelineTableViewController: UIViewController {
|
||||
}
|
||||
.margins(.all, self.timelineStyle.rowInsets)
|
||||
.minSize(height: 1)
|
||||
.background(Color.clear)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
@@ -82,12 +82,12 @@ struct TimelineView: UIViewControllerRepresentable {
|
||||
// MARK: - Previews
|
||||
|
||||
struct TimelineTableView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
roomName: "Preview room")
|
||||
|
||||
static var previews: some View {
|
||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
roomName: "Preview room")
|
||||
|
||||
NavigationView {
|
||||
RoomScreen(context: viewModel.context)
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ struct SettingsScreen: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.background(backgroundColor, ignoresSafeAreaEdges: .all)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
closeButton
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
doneButton
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,8 +152,8 @@ struct SettingsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button(ElementL10n.actionCancel, action: close)
|
||||
private var doneButton: some View {
|
||||
Button(ElementL10n.done, action: close)
|
||||
.accessibilityIdentifier("closeButton")
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
}
|
||||
|
||||
return EncryptedRoomTimelineItem(id: eventItemProxy.id,
|
||||
text: ElementL10n.encryptionInformationDecryptionError,
|
||||
text: ElementL10n.roomTimelineUnableToDecrypt,
|
||||
encryptionType: encryptionType,
|
||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||
groupState: groupState,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Update the designs for the timeline.
|
||||
Reference in New Issue
Block a user