mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
Room screen: timeline message bubbles (#91)
* #34 Create `TimelineItemStylerView` * #34 Add styler view into different type of cells * #34 Use placeholder avatar image on room list * #34 Add `isOutgoing` param to event based timeline item * #34 Compute `isOutgoing` for timeline items * #34 Update sender info view in timeline * #34 Update mock timeline items * #34 Rename `EventBasedTimelineView` to `EventBasedTimelineSenderView` * #34 Change padding on timeline cells * #34 Create `TimelineItemStylerView` to move content into a bubble if needed * #34 Use styler view in all of the timeline item views * #34 Make timestamp more readable on images * #34 Little layout tweaks * #34 Add changelog * #34 Fix code smells * #34 Set text colors on timeline items * #34 Fix background color of the timeline * #34 Fix PR remarks * #34 Set background colors explicitly on remaining screens * #34 Reduce min bubble width and make it a scaled metric * #34 Refactor `PlaceholderAvatarImage` to accept a text only * #34 Fix code smell * #34 Fix further comments
This commit is contained in:
parent
5df1411a7e
commit
b6b8b4be26
@ -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 = "<group>"; };
|
||||
55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineView.swift; sourceTree = "<group>"; };
|
||||
56008790A9C4479A6B31FDF4 /* EventBasedTimelineSenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineSenderView.swift; sourceTree = "<group>"; };
|
||||
56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = "<group>"; };
|
||||
5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = "<group>"; };
|
||||
@ -576,6 +577,7 @@
|
||||
E8FD25EB4DF66625B74E4505 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
EC81289F28620A970012F05B /* TimelineItemStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStylerView.swift; sourceTree = "<group>"; };
|
||||
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
EDB3E99D445CFCB3AA3F34FB /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
|
||||
EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -34,6 +34,7 @@ struct RoomScreen: View {
|
||||
RoomHeaderView(context: context)
|
||||
}
|
||||
}
|
||||
.background(Color.element.background, ignoresSafeAreaEdges: .all)
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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<Header: View, Content: View>: 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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -13,11 +13,31 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
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<Void, RoomTimelineControllerError> {
|
||||
return .failure(.generic)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
1
changelog.d/34.change
Normal file
1
changelog.d/34.change
Normal file
@ -0,0 +1 @@
|
||||
Room: Use bubbles in the timeline.
|
Loading…
x
Reference in New Issue
Block a user