diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index aa00ab137..426a7cb1e 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -72,7 +72,7 @@ 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; 3772354754450F2B54107E17 /* TemplateSimpleScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4EDB32B97910AAAFE632B2 /* TemplateSimpleScreenViewModelProtocol.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; - 39AE84C8E5F2FE9D2DC7775C /* EventBasedTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */; }; + 39AE84C8E5F2FE9D2DC7775C /* EventBasedTimelineSenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56008790A9C4479A6B31FDF4 /* EventBasedTimelineSenderView.swift */; }; 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; }; 3C549A0BF39F8A854D45D9FD /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4411C0DA0087A1CB143E96FA /* EventBrief.swift */; }; @@ -222,6 +222,7 @@ EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; }; + EC8128A028620A970012F05B /* TimelineItemStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC81289F28620A970012F05B /* TimelineItemStylerView.swift */; }; ED4F663C783E9A8C0E80B983 /* TemplateSimpleScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47543EB19F3DCF308751F53C /* TemplateSimpleScreenViewModel.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; @@ -376,7 +377,7 @@ 534A5C8FCDE2CBC50266B9F2 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = gl; path = gl.lproj/Localizable.stringsdict; sourceTree = ""; }; 55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = ""; }; 55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = ""; }; - 56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineView.swift; sourceTree = ""; }; + 56008790A9C4479A6B31FDF4 /* EventBasedTimelineSenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineSenderView.swift; sourceTree = ""; }; 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = ""; }; 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; 5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = ""; }; @@ -576,6 +577,7 @@ E8FD25EB4DF66625B74E4505 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = ""; }; E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; }; + EC81289F28620A970012F05B /* TimelineItemStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStylerView.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; EDB3E99D445CFCB3AA3F34FB /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -1031,6 +1033,7 @@ B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */, 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */, 874A1842477895F199567BD7 /* TimelineView.swift */, + EC81289F28620A970012F05B /* TimelineItemStylerView.swift */, B7D3886505ECC85A06DA8258 /* Timeline */, ); path = View; @@ -1250,7 +1253,7 @@ isa = PBXGroup; children = ( 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */, - 56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */, + 56008790A9C4479A6B31FDF4 /* EventBasedTimelineSenderView.swift */, F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */, D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */, B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */, @@ -1733,7 +1736,7 @@ 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */, 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, - 39AE84C8E5F2FE9D2DC7775C /* EventBasedTimelineView.swift in Sources */, + 39AE84C8E5F2FE9D2DC7775C /* EventBasedTimelineSenderView.swift in Sources */, 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */, 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */, F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */, @@ -1843,6 +1846,7 @@ 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */, 4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */, 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */, + EC8128A028620A970012F05B /* TimelineItemStylerView.swift in Sources */, 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */, 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */, 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */, diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 5e337e516..322799339 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -238,6 +238,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { MXLog.error("Invalid room identifier: \(roomIdentifier)") return } + let userId = userSession.clientProxy.userIdentifier let memberDetailProvider = memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy) @@ -245,7 +246,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { memberDetailProvider: memberDetailProvider, attributedStringBuilder: AttributedStringBuilder()) - let timelineController = RoomTimelineController(timelineProvider: RoomTimelineProvider(roomProxy: roomProxy), + let timelineController = RoomTimelineController(userId: userId, + timelineProvider: RoomTimelineProvider(roomProxy: roomProxy), timelineItemFactory: timelineItemFactory, mediaProvider: userSession.mediaProvider, memberDetailProvider: memberDetailProvider) diff --git a/ElementX/Sources/Screens/BugReport/View/BugReport.swift b/ElementX/Sources/Screens/BugReport/View/BugReport.swift index 4c793c0d8..fb5809a29 100644 --- a/ElementX/Sources/Screens/BugReport/View/BugReport.swift +++ b/ElementX/Sources/Screens/BugReport/View/BugReport.swift @@ -50,6 +50,7 @@ struct BugReport: View { } .navigationTitle(ElementL10n.titleActivityBugReport) } + .background(Color.element.background, ignoresSafeAreaEdges: .all) } /// The main content of the view to be shown in a scroll view. @@ -59,11 +60,11 @@ struct BugReport: View { .accessibilityIdentifier("reportBugDescription") ZStack(alignment: .topLeading) { RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(Color(UIColor.secondarySystemBackground)) + .fill(Color.element.system) if context.reportText.isEmpty { Text(ElementL10n.sendBugReportPlaceholder) - .foregroundColor(Color(UIColor.placeholderText)) + .foregroundColor(Color.element.secondaryContent) .padding(.horizontal, 8) .padding(.vertical, 12) } @@ -131,10 +132,14 @@ struct BugReport: View { struct BugReport_Previews: PreviewProvider { static var previews: some View { - Group { - let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image) - BugReport(context: viewModel.context) - .previewInterfaceOrientation(.portrait) - } + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + @ViewBuilder + static var body: some View { + let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image) + BugReport(context: viewModel.context) + .previewInterfaceOrientation(.portrait) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 6d6823da8..674ddb706 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -34,6 +34,7 @@ struct HomeScreen: View { Section("Rooms") { ForEach(context.viewState.unencryptedRooms) { room in RoomCell(room: room, context: context) + .listRowBackground(Color.clear) } let other = context.viewState.encryptedRooms @@ -44,6 +45,7 @@ struct HomeScreen: View { RoomCell(room: room, context: context) } } + .listRowBackground(Color.clear) } } @@ -60,6 +62,7 @@ struct HomeScreen: View { RoomCell(room: room, context: context) } } + .listRowBackground(Color.clear) } } } @@ -69,6 +72,7 @@ struct HomeScreen: View { Spacer() } + .background(Color.element.background) .ignoresSafeArea(.all, edges: .bottom) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -127,7 +131,8 @@ struct RoomCell: View { .frame(width: 40, height: 40) .mask(Circle()) } else { - Image(systemName: "person.3") + PlaceholderAvatarImage(text: room.displayName ?? room.id) + .clipShape(Circle()) .frame(width: 40, height: 40) } @@ -168,6 +173,11 @@ struct RoomCell: View { struct HomeScreen_Previews: PreviewProvider { static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + static var body: some View { let viewModel = HomeScreenViewModel(attributedStringBuilder: AttributedStringBuilder()) let eventBrief = EventBrief(eventId: "id", diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift index ff4642d14..8a258abcb 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift @@ -45,7 +45,7 @@ struct RoomHeaderView: View { .scaledToFill() .accessibilityIdentifier("roomAvatarImage") } else { - PlaceholderAvatarImage(firstCharacter: String(context.viewState.roomTitle.first ?? Character(""))) + PlaceholderAvatarImage(text: context.viewState.roomTitle) .accessibilityIdentifier("roomAvatarPlaceholderImage") } } @@ -78,7 +78,7 @@ struct RoomHeaderView_Previews: PreviewProvider { let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(), timelineViewFactory: RoomTimelineViewFactory(), roomName: "Some Room name", - roomAvatar: Asset.Images.appLogo.image, + roomAvatar: nil, roomEncryptionBadge: Asset.Images.encryptionTrusted.image ) diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index e96b0ceaa..7e6137843 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -34,6 +34,7 @@ struct RoomScreen: View { RoomHeaderView(context: context) } } + .background(Color.element.background, ignoresSafeAreaEdges: .all) } private func sendMessage() { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift index e1419c011..c2e893ecf 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift @@ -14,13 +14,17 @@ struct EmoteRoomTimelineView: View { var body: some View { VStack(alignment: .leading) { - EventBasedTimelineView(timelineItem: timelineItem) - HStack(alignment: .top) { - Image(systemName: "face.dashed").padding(.top, 1.0) - if let attributedComponents = timelineItem.attributedComponents { - FormattedBodyText(attributedComponents: attributedComponents) - } else { - Text(timelineItem.text) + TimelineItemStylerView(timelineItem: timelineItem) { + EventBasedTimelineSenderView(timelineItem: timelineItem) + } content: { + HStack(alignment: .top) { + Image(systemName: "face.dashed").padding(.top, 1.0) + if let attributedComponents = timelineItem.attributedComponents { + FormattedBodyText(attributedComponents: attributedComponents) + } else { + Text(timelineItem.text) + .foregroundColor(.element.primaryContent) + } } } } @@ -52,6 +56,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider { text: text, timestamp: timestamp, shouldShowSenderDetails: true, + isOutgoing: false, senderId: senderId) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EventBasedTimelineSenderView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EventBasedTimelineSenderView.swift new file mode 100644 index 000000000..82306117e --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EventBasedTimelineSenderView.swift @@ -0,0 +1,93 @@ +// +// EventBasedTimelineSenderView.swift +// ElementX +// +// Created by Stefan Ceriu on 18/03/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import SwiftUI + +struct EventBasedTimelineSenderView: View { + let timelineItem: EventBasedTimelineItemProtocol + + @ScaledMetric private var avatarSize = 26 + + var body: some View { + if timelineItem.shouldShowSenderDetails { + VStack { + Spacer() + .frame(height: 8) + HStack(alignment: .top, spacing: 4) { + avatar + Text(timelineItem.senderDisplayName ?? timelineItem.senderId) + .font(.body) + .foregroundColor(.element.primaryContent) + .fontWeight(.semibold) + .lineLimit(1) + } + } + } + } + + @ViewBuilder private var avatar: some View { + ZStack(alignment: .center) { + if let avatar = timelineItem.senderAvatar { + Image(uiImage: avatar) + .resizable() + .scaledToFill() + .overlay(Circle().stroke(Color.element.accent)) + } else { + PlaceholderAvatarImage(text: timelineItem.senderDisplayName ?? timelineItem.senderId) + } + } + .clipShape(Circle()) + .frame(width: avatarSize, height: avatarSize) + .overlay( + Circle() + .stroke(Color.element.background, lineWidth: 2) + ) + + .animation(.default, value: timelineItem.senderAvatar) + } +} + +struct EventBasedTimelineSenderView_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + @ViewBuilder + static var body: some View { + VStack(alignment: .leading, spacing: 20.0) { + EventBasedTimelineSenderView(timelineItem: item1) + + EventBasedTimelineSenderView(timelineItem: item2) + } + .frame(maxHeight: 160) + .previewLayout(.sizeThatFits) + } + + private static var item1: EventBasedTimelineItemProtocol { + TextRoomTimelineItem(id: UUID().uuidString, + text: "Some text", + timestamp: "", + shouldShowSenderDetails: true, + isOutgoing: false, + senderId: "", + senderDisplayName: "Bob") + } + + private static var item2: EventBasedTimelineItemProtocol { + TextRoomTimelineItem(id: UUID().uuidString, + text: "Some text", + timestamp: "", + shouldShowSenderDetails: true, + isOutgoing: false, + senderId: "", + senderDisplayName: "Some long display name for a user") + } + +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EventBasedTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EventBasedTimelineView.swift deleted file mode 100644 index 4a8e6ef68..000000000 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EventBasedTimelineView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// EventBasedTimelineView.swift -// ElementX -// -// Created by Stefan Ceriu on 18/03/2022. -// Copyright © 2022 Element. All rights reserved. -// - -import Foundation -import SwiftUI - -struct EventBasedTimelineView: View { - let timelineItem: EventBasedTimelineItemProtocol - - var body: some View { - if timelineItem.shouldShowSenderDetails { - HStack { - avatar - Text(timelineItem.senderDisplayName ?? timelineItem.senderId) - .font(.footnote) - .bold() - Text(timelineItem.timestamp) - .font(.footnote) - } - } - } - - @ViewBuilder private var avatar: some View { - ZStack(alignment: .center) { - if let avatar = timelineItem.senderAvatar { - Image(uiImage: avatar) - .resizable() - .scaledToFill() - .overlay(Circle().stroke(Color.element.accent)) - } else { - PlaceholderAvatarImage(firstCharacter: String(firstLetter)) - } - } - .clipShape(Circle()) - .frame(width: 24.0, height: 24.0) - .animation(.default, value: timelineItem.senderAvatar) - } - - private var firstLetter: String { - if let senderDisplayName = timelineItem.senderDisplayName { - return senderDisplayName.prefix(1).uppercased() - } else { - return timelineItem.senderId.prefix(2).suffix(1).uppercased() - } - } -} - -struct EventBasedTimelineView_Previews: PreviewProvider { - static var previews: some View { - body.preferredColorScheme(.light) - body.preferredColorScheme(.dark) - } - - @ViewBuilder - static var body: some View { - VStack(alignment: .leading, spacing: 20.0) { - EventBasedTimelineView(timelineItem: itemWith(text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.", - timestamp: "Now", - senderId: "Bob")) - - EventBasedTimelineView(timelineItem: itemWith(text: "Some other text", - timestamp: "Later", - senderId: "Anne")) - } - } - - private static func itemWith(text: String, timestamp: String, senderId: String) -> TextRoomTimelineItem { - return TextRoomTimelineItem(id: UUID().uuidString, - text: text, - timestamp: timestamp, - shouldShowSenderDetails: true, - senderId: senderId) - } -} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift index 13aaba784..1d1dc6732 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift @@ -22,10 +22,12 @@ struct FormattedBodyText: View { .frame(width: 4.0) Text(component.attributedString) .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.element.primaryContent) } } else { Text(component.attributedString) .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.element.primaryContent) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift index 990dddaa0..355c4cfde 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift @@ -15,36 +15,40 @@ struct ImageRoomTimelineView: View { var body: some View { if timelineItem.image != nil || timelineItem.blurhash != nil { // Fixes view heights after loading finishes VStack(alignment: .leading) { - EventBasedTimelineView(timelineItem: timelineItem) - Text(timelineItem.text) - if let image = timelineItem.image { - if let aspectRatio = timelineItem.aspectRatio { + TimelineItemStylerView(timelineItem: timelineItem) { + EventBasedTimelineSenderView(timelineItem: timelineItem) + } content: { + if let image = timelineItem.image { + if let aspectRatio = timelineItem.aspectRatio { + Image(uiImage: image) + .resizable() + .aspectRatio(aspectRatio, contentMode: .fit) + } else { + Image(uiImage: image) + .resizable() + .scaledToFit() + } + } else if let blurhash = timelineItem.blurhash, + // Build a small blurhash image so that it's fast + let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) { Image(uiImage: image) .resizable() - .aspectRatio(aspectRatio, contentMode: .fit) - } else { - Image(uiImage: image) - .resizable() - .scaledToFit() + .aspectRatio(timelineItem.aspectRatio, contentMode: .fit) } - } else if let blurhash = timelineItem.blurhash, - // Build a small blurhash image so that it's fast - let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) { - Image(uiImage: image) - .resizable() - .aspectRatio(timelineItem.aspectRatio, contentMode: .fit) } } .animation(.default, value: timelineItem.image) .frame(maxHeight: 1000.0) } else { VStack(alignment: .leading) { - EventBasedTimelineView(timelineItem: timelineItem) - Text(timelineItem.text) - HStack { - Spacer() - ProgressView("Loading") - Spacer() + TimelineItemStylerView(timelineItem: timelineItem) { + EventBasedTimelineSenderView(timelineItem: timelineItem) + } content: { + HStack { + Spacer() + ProgressView("Loading") + Spacer() + } } } } @@ -64,6 +68,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { text: "Some image", timestamp: "Now", shouldShowSenderDetails: false, + isOutgoing: false, senderId: "Bob", source: nil, image: UIImage(systemName: "photo"))) @@ -72,6 +77,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { text: "Some other image", timestamp: "Now", shouldShowSenderDetails: false, + isOutgoing: false, senderId: "Bob", source: nil, image: nil)) @@ -80,6 +86,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { text: "Blurhashed image", timestamp: "Now", shouldShowSenderDetails: false, + isOutgoing: false, senderId: "Bob", source: nil, image: nil, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift index 58ebce134..c2b39c2c8 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift @@ -14,13 +14,17 @@ struct NoticeRoomTimelineView: View { var body: some View { VStack(alignment: .leading) { - EventBasedTimelineView(timelineItem: timelineItem) - HStack(alignment: .top) { - Image(systemName: "exclamationmark.bubble").padding(.top, 2.0) - if let attributedComponents = timelineItem.attributedComponents { - FormattedBodyText(attributedComponents: attributedComponents) - } else { - Text(timelineItem.text) + TimelineItemStylerView(timelineItem: timelineItem) { + EventBasedTimelineSenderView(timelineItem: timelineItem) + } content: { + HStack(alignment: .top) { + Image(systemName: "exclamationmark.bubble").padding(.top, 2.0) + if let attributedComponents = timelineItem.attributedComponents { + FormattedBodyText(attributedComponents: attributedComponents) + } else { + Text(timelineItem.text) + .foregroundColor(.element.primaryContent) + } } } } @@ -50,9 +54,10 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider { private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem { return NoticeRoomTimelineItem(id: UUID().uuidString, - text: text, - timestamp: timestamp, - shouldShowSenderDetails: true, - senderId: senderId) + text: text, + timestamp: timestamp, + shouldShowSenderDetails: true, + isOutgoing: false, + senderId: senderId) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift index 508a16481..e36e79492 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift @@ -18,12 +18,12 @@ import SwiftUI struct PlaceholderAvatarImage: View { - let firstCharacter: String + private let textForImage: String var body: some View { ZStack { Color.element.accent - Text(firstCharacter) + Text(textForImage) .padding(4) .foregroundColor(.white) // Make the text resizable (i.e. Make it large and then allow it to scale down) @@ -32,6 +32,10 @@ struct PlaceholderAvatarImage: View { } .aspectRatio(1, contentMode: .fill) } + + init(text: String) { + textForImage = text.first?.uppercased() ?? "" + } } struct PlaceholderAvatarImage_Previews: PreviewProvider { @@ -42,7 +46,7 @@ struct PlaceholderAvatarImage_Previews: PreviewProvider { @ViewBuilder static var body: some View { - PlaceholderAvatarImage(firstCharacter: "X") + PlaceholderAvatarImage(text: "X") .clipShape(Circle()) .frame(width: 150, height: 100) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift index 7aabc7601..0a268bfc1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift @@ -15,6 +15,7 @@ struct SeparatorRoomTimelineView: View { var body: some View { LabelledDivider(label: timelineItem.text) .id(timelineItem.id) + .padding(.vertical, 8) } } @@ -22,7 +23,7 @@ struct LabelledDivider: View { let label: String let color: Color - init(label: String, color: Color = .gray) { + init(label: String, color: Color = Color.element.secondaryContent) { self.label = label self.color = color } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift index d71dbefb9..60ea56e74 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift @@ -13,12 +13,15 @@ struct TextRoomTimelineView: View { let timelineItem: TextRoomTimelineItem var body: some View { - VStack(alignment: .leading) { - EventBasedTimelineView(timelineItem: timelineItem) + TimelineItemStylerView(timelineItem: timelineItem) { + EventBasedTimelineSenderView(timelineItem: timelineItem) + } content: { if let attributedComponents = timelineItem.attributedComponents { FormattedBodyText(attributedComponents: attributedComponents) } else { Text(timelineItem.text) + .font(.body) + .multilineTextAlignment(.leading) } } .id(timelineItem.id) @@ -36,19 +39,25 @@ struct TextRoomTimelineView_Previews: PreviewProvider { VStack(alignment: .leading, spacing: 20.0) { TextRoomTimelineView(timelineItem: itemWith(text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.", timestamp: "Now", + shouldShowSenderDetails: true, + isOutgoing: false, senderId: "Bob")) TextRoomTimelineView(timelineItem: itemWith(text: "Some other text", timestamp: "Later", + shouldShowSenderDetails: true, + isOutgoing: true, senderId: "Anne")) } + .padding(.horizontal, 8) } - private static func itemWith(text: String, timestamp: String, senderId: String) -> TextRoomTimelineItem { + private static func itemWith(text: String, timestamp: String, shouldShowSenderDetails: Bool, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem { return TextRoomTimelineItem(id: UUID().uuidString, - text: text, - timestamp: timestamp, - shouldShowSenderDetails: true, - senderId: senderId) + text: text, + timestamp: timestamp, + shouldShowSenderDetails: shouldShowSenderDetails, + isOutgoing: isOutgoing, + senderId: senderId) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift index 762d8d2eb..2cfe3838a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift @@ -32,6 +32,7 @@ struct TimelineItemList: View { .animation(.default, value: context.viewState.isBackPaginating) Spacer() } + .listRowBackground(Color.clear) // No idea why previews don't work otherwise ForEach(isPreview ? context.viewState.items : timelineItems) { timelineItem in @@ -39,7 +40,9 @@ struct TimelineItemList: View { .contextMenu(menuItems: { context.viewState.contextMenuBuilder?(timelineItem.id) }) + .listRowBackground(Color.clear) .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8)) .onAppear { context.send(viewAction: .itemAppeared(id: timelineItem.id)) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemStylerView.swift new file mode 100644 index 000000000..302d238ca --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemStylerView.swift @@ -0,0 +1,158 @@ +// +// TimelineItemStyleView.swift +// ElementX +// +// Created by Ismail on 21.06.2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import SwiftUI +import Combine + +import Introspect + +struct TimelineItemStylerView: View { + + let timelineItem: EventBasedTimelineItemProtocol + @ViewBuilder let header: () -> Header + @ViewBuilder let content: () -> Content + + @Environment(\.colorScheme) private var colorScheme + @ScaledMetric private var minBubbleWidth = 44 + + var body: some View { + VStack(alignment: timelineItem.isOutgoing ? .trailing : .leading, spacing: -5) { + if !timelineItem.isOutgoing { + header() + .zIndex(1) + } + if timelineItem.isOutgoing { + HStack { + Spacer() + styledContent + } + .padding(.trailing, 16) + .padding(.leading, 51) + } else { + styledContent + .padding(.leading, 16) + .padding(.trailing, 51) + } + } + } + + @ViewBuilder + var styledContent: some View { + if shouldAvoidBubbling { + ZStack(alignment: .bottomTrailing) { + content() + .clipped() + .cornerRadius(8) + Text(timelineItem.timestamp) + .foregroundColor(.global.white) + .font(.element.caption2) + .padding(4) + .background(Color(white: 0, opacity: 0.7)) + .clipped() + .cornerRadius(8) + .offset(x: -8, y: -8) + } + } else { + VStack(alignment: .trailing, spacing: 4) { + content() + .frame(minWidth: minBubbleWidth, alignment: .leading) + + Text(timelineItem.timestamp) + .foregroundColor(Color.element.tertiaryContent) + .font(.element.caption2) + } + .padding(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8)) + .clipped() + .background(bubbleColor) + .cornerRadius(12) + } + } + + private var shouldAvoidBubbling: Bool { + return timelineItem is ImageRoomTimelineItem + } + + private var bubbleColor: Color { + let opacity = colorScheme == .light ? 0.06 : 0.15 + return timelineItem.isOutgoing ? .element.accent.opacity(opacity) : .element.system + } + +} + +struct TimelineItemStylerView_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + @ViewBuilder + static var body: some View { + VStack(alignment: .leading) { + TimelineItemStylerView(timelineItem: item1) { + EventBasedTimelineSenderView(timelineItem: item1) + } content: { + Text(item1.text) + } + TimelineItemStylerView(timelineItem: item2) { + EventBasedTimelineSenderView(timelineItem: item2) + } content: { + Text(item2.text) + } + TimelineItemStylerView(timelineItem: item3) { + EventBasedTimelineSenderView(timelineItem: item3) + } content: { + Text(item3.text) + } + TimelineItemStylerView(timelineItem: item4) { + EventBasedTimelineSenderView(timelineItem: item4) + } content: { + Text(item4.text) + } + } + .padding(.horizontal, 8) + .frame(maxHeight: 400) + .previewLayout(.sizeThatFits) + } + + private static var item1: TextRoomTimelineItem { + return TextRoomTimelineItem(id: UUID().uuidString, + text: "Short", + timestamp: "07:05", + shouldShowSenderDetails: true, + isOutgoing: false, + senderId: "Bob") + } + + private static var item2: TextRoomTimelineItem { + return TextRoomTimelineItem(id: UUID().uuidString, + text: "Short loin ground round tongue hamburger, fatback salami shoulder.", + timestamp: "08:05", + shouldShowSenderDetails: true, + isOutgoing: false, + senderId: "Bob") + } + + private static var item3: TextRoomTimelineItem { + return TextRoomTimelineItem(id: UUID().uuidString, + text: "Short loin ground round tongue hamburger, fatback salami shoulder.", + timestamp: "08:07", + shouldShowSenderDetails: false, + isOutgoing: true, + senderId: "Bob") + } + + private static var item4: TextRoomTimelineItem { + return TextRoomTimelineItem(id: UUID().uuidString, + text: "Short", + timestamp: "08:08", + shouldShowSenderDetails: false, + isOutgoing: true, + senderId: "Bob") + } +} diff --git a/ElementX/Sources/Screens/Settings/View/Settings.swift b/ElementX/Sources/Screens/Settings/View/Settings.swift index e9794790e..e093afc4c 100644 --- a/ElementX/Sources/Screens/Settings/View/Settings.swift +++ b/ElementX/Sources/Screens/Settings/View/Settings.swift @@ -21,6 +21,7 @@ struct Settings: View { // MARK: Private @State private var showingLogoutConfirmation = false + @Environment(\.colorScheme) private var colorScheme // MARK: Public @@ -41,9 +42,11 @@ struct Settings: View { Button("Crash the app", role: .destructive) { context.send(viewAction: .crash) } + .accessibilityIdentifier("crashButton") } } + .listRowBackground(rowBackgroundColor) Section { Button { showingLogoutConfirmation = true } label: { @@ -62,23 +65,40 @@ struct Settings: View { } footer: { versionText } + .listRowBackground(rowBackgroundColor) + } + .introspectTableView { tableView in + tableView.backgroundColor = .clear } .navigationTitle(ElementL10n.settings) + .background(backgroundColor, ignoresSafeAreaEdges: .all) } var versionText: some View { Text(ElementL10n.settingsVersion + ": " + ElementInfoPlist.cfBundleShortVersionString + " (" + ElementInfoPlist.cfBundleVersion + ")") } + + private var backgroundColor: Color { + colorScheme == .light ? .element.system : .element.background + } + + private var rowBackgroundColor: Color { + colorScheme == .light ? .element.background : .element.system + } } // MARK: - Previews struct Settings_Previews: PreviewProvider { static var previews: some View { - Group { - let viewModel = SettingsViewModel() - Settings(context: viewModel.context) - .previewInterfaceOrientation(.portrait) - } + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + @ViewBuilder + static var body: some View { + let viewModel = SettingsViewModel() + Settings(context: viewModel.context) + .previewInterfaceOrientation(.portrait) } } diff --git a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift index cc724e069..d1ff65908 100644 --- a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift @@ -13,11 +13,31 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { let callbacks = PassthroughSubject() - var timelineItems: [RoomTimelineItemProtocol] = [SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Yesterday"), - TextRoomTimelineItem(id: UUID().uuidString, text: "You rock!", timestamp: "10:10 AM", shouldShowSenderDetails: true, senderId: "Alice"), - TextRoomTimelineItem(id: UUID().uuidString, text: "You also rule!", timestamp: "10:11 AM", shouldShowSenderDetails: false, senderId: "Alice"), - SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Today"), - TextRoomTimelineItem(id: UUID().uuidString, text: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true, senderId: "Bob")] + var timelineItems: [RoomTimelineItemProtocol] = [SeparatorRoomTimelineItem(id: UUID().uuidString, + text: "Yesterday"), + TextRoomTimelineItem(id: UUID().uuidString, + text: "You rock!", + timestamp: "10:10 AM", + shouldShowSenderDetails: true, + isOutgoing: false, + senderId: "", + senderDisplayName: "Some user with a really long long long long long display name"), + TextRoomTimelineItem(id: UUID().uuidString, + text: "You also rule!", + timestamp: "10:11 AM", + shouldShowSenderDetails: false, + isOutgoing: false, + senderId: "", + senderDisplayName: "Alice"), + SeparatorRoomTimelineItem(id: UUID().uuidString, + text: "Today"), + TextRoomTimelineItem(id: UUID().uuidString, + text: "You too!", + timestamp: "5 PM", + shouldShowSenderDetails: false, + isOutgoing: true, + senderId: "", + senderDisplayName: "Bob")] func paginateBackwards(_ count: UInt) async -> Result { return .failure(.generic) diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index 94ce2aaa1..5c3fe6f8b 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -11,6 +11,7 @@ import Combine import UIKit class RoomTimelineController: RoomTimelineControllerProtocol { + private let userId: String private let timelineProvider: RoomTimelineProviderProtocol private let timelineItemFactory: RoomTimelineItemFactoryProtocol private let mediaProvider: MediaProviderProtocol @@ -22,10 +23,12 @@ class RoomTimelineController: RoomTimelineControllerProtocol { private(set) var timelineItems = [RoomTimelineItemProtocol]() - init(timelineProvider: RoomTimelineProviderProtocol, + init(userId: String, + timelineProvider: RoomTimelineProviderProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol, memberDetailProvider: MemberDetailProviderProtocol) { + self.userId = userId self.timelineProvider = timelineProvider self.timelineItemFactory = timelineItemFactory self.mediaProvider = mediaProvider @@ -110,7 +113,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol { let areMessagesFromTheSameSender = (previousMessage?.sender == message.sender) let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay - newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(message: message, showSenderDetails: shouldShowSenderDetails)) + newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(message: message, + isOutgoing: message.sender == userId, + showSenderDetails: shouldShowSenderDetails)) previousMessage = message } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index 891f0f002..4830d92ef 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -13,6 +13,7 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol { var text: String { get } var timestamp: String { get } var shouldShowSenderDetails: Bool { get } + var isOutgoing: Bool { get } var senderId: String { get } var senderDisplayName: String? { get set } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/EmoteRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/EmoteRoomTimelineItem.swift index 04c5edd38..60c474241 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/EmoteRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/EmoteRoomTimelineItem.swift @@ -15,6 +15,7 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa var attributedComponents: [AttributedStringBuilderComponent]? let timestamp: String let shouldShowSenderDetails: Bool + let isOutgoing: Bool let senderId: String var senderDisplayName: String? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift index be92ed341..fd891fb88 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift @@ -14,6 +14,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa let text: String let timestamp: String let shouldShowSenderDetails: Bool + let isOutgoing: Bool let senderId: String var senderDisplayName: String? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/NoticeRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/NoticeRoomTimelineItem.swift index c9c4d7919..00419c070 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/NoticeRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/NoticeRoomTimelineItem.swift @@ -15,6 +15,7 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equ var attributedComponents: [AttributedStringBuilderComponent]? let timestamp: String let shouldShowSenderDetails: Bool + let isOutgoing: Bool let senderId: String var senderDisplayName: String? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/TextRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/TextRoomTimelineItem.swift index f5da6945c..41670afaa 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/TextRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/TextRoomTimelineItem.swift @@ -15,6 +15,7 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat var attributedComponents: [AttributedStringBuilderComponent]? let timestamp: String let shouldShowSenderDetails: Bool + let isOutgoing: Bool let senderId: String var senderDisplayName: String? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 5ce473070..9116db0b7 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -22,20 +22,20 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { self.attributedStringBuilder = attributedStringBuilder } - func buildTimelineItemFor(message: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol { + func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) -> RoomTimelineItemProtocol { let displayName = memberDetailProvider.displayNameForUserId(message.sender) let avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender) let avatarImage = mediaProvider.imageFromURLString(avatarURL) switch message { case let message as TextRoomMessage: - return buildTextTimelineItemFromMessage(message, showSenderDetails, displayName, avatarImage) + return buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) case let message as ImageRoomMessage: - return buildImageTimelineItemFromMessage(message, showSenderDetails, displayName, avatarImage) + return buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) case let message as NoticeRoomMessage: - return buildNoticeTimelineItemFromMessage(message, showSenderDetails, displayName, avatarImage) + return buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) case let message as EmoteRoomMessage: - return buildEmoteTimelineItemFromMessage(message, showSenderDetails, displayName, avatarImage) + return buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) default: fatalError("Unknown room message.") } @@ -43,6 +43,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { // MARK: - Private private func buildTextTimelineItemFromMessage(_ message: TextRoomMessage, + _ isOutgoing: Bool, _ showSenderDetails: Bool, _ displayName: String?, _ avatarImage: UIImage?) -> RoomTimelineItemProtocol { @@ -54,12 +55,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { attributedComponents: attributedComponents, timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened), shouldShowSenderDetails: showSenderDetails, + isOutgoing: isOutgoing, senderId: message.sender, senderDisplayName: displayName, senderAvatar: avatarImage) } private func buildImageTimelineItemFromMessage(_ message: ImageRoomMessage, + _ isOutgoing: Bool, _ showSenderDetails: Bool, _ displayName: String?, _ avatarImage: UIImage?) -> RoomTimelineItemProtocol { @@ -74,6 +77,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { text: message.body, timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened), shouldShowSenderDetails: showSenderDetails, + isOutgoing: isOutgoing, senderId: message.sender, senderDisplayName: displayName, senderAvatar: avatarImage, @@ -86,6 +90,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildNoticeTimelineItemFromMessage(_ message: NoticeRoomMessage, + _ isOutgoing: Bool, _ showSenderDetails: Bool, _ displayName: String?, _ avatarImage: UIImage?) -> RoomTimelineItemProtocol { @@ -97,12 +102,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { attributedComponents: attributedComponents, timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened), shouldShowSenderDetails: showSenderDetails, + isOutgoing: isOutgoing, senderId: message.sender, senderDisplayName: displayName, senderAvatar: avatarImage) } private func buildEmoteTimelineItemFromMessage(_ message: EmoteRoomMessage, + _ isOutgoing: Bool, _ showSenderDetails: Bool, _ displayName: String?, _ avatarImage: UIImage?) -> RoomTimelineItemProtocol { @@ -114,6 +121,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { attributedComponents: attributedComponents, timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened), shouldShowSenderDetails: showSenderDetails, + isOutgoing: isOutgoing, senderId: message.sender, senderDisplayName: displayName, senderAvatar: avatarImage) diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift index 0afc05d42..a0eaa40a5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift @@ -10,5 +10,5 @@ import Foundation @MainActor protocol RoomTimelineItemFactoryProtocol { - func buildTimelineItemFor(message: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol + func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) -> RoomTimelineItemProtocol } diff --git a/changelog.d/34.change b/changelog.d/34.change new file mode 100644 index 000000000..afb7b62f2 --- /dev/null +++ b/changelog.d/34.change @@ -0,0 +1 @@ +Room: Use bubbles in the timeline.