Timeline item sender profiles (#463)

* Adopt timeline item sender profiles for displaying user avatars and display names
* Prevent images from automatically being evicted from the in-memory cache when entering background
* Get rid of all the `urlString: String`s and start using proper URLs
* Add back manual display name loading as the sender profiles aren't working properly yet
* Implement a sender profile.
* Bump the RustSDK to v1.0.32-alpha

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Stefan Ceriu
2023-01-18 16:29:44 +02:00
committed by GitHub
parent 8f906c7be7
commit 8f3f83167b
70 changed files with 390 additions and 495 deletions
@@ -101,7 +101,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
var lastMessage: AttributedString?
var avatarURLString: String?
var avatarURL: URL?
var avatar: UIImage?
@@ -68,8 +68,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
.store(in: &cancellables)
Task {
if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, avatarSize: .user(on: .home)) {
if case let .success(url) = await userSession.clientProxy.loadUserAvatarURL() {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURL(url, avatarSize: .user(on: .home)) {
state.userAvatar = avatar
}
}
@@ -169,12 +169,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
private func loadDataForRoomIdentifier(_ identifier: String) {
guard let room = state.rooms.first(where: { $0.roomId == identifier }),
room.avatar == nil,
let avatarURLString = room.avatarURLString else {
let avatarURL = room.avatarURL else {
return
}
Task {
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) {
if case let .success(image) = await userSession.mediaProvider.loadImageFromURL(avatarURL, avatarSize: .room(on: .home)) {
guard let roomIndex = state.rooms.firstIndex(where: { $0.roomId == identifier }) else {
return
}
@@ -222,7 +222,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
}
private func buildRoom(with details: RoomSummaryDetails, invalidated: Bool) -> HomeScreenRoom {
let avatarImage = userSession.mediaProvider.imageFromURLString(details.avatarURLString, avatarSize: .room(on: .home))
let avatarImage = details.avatarURL.flatMap { userSession.mediaProvider.imageFromURL($0, avatarSize: .room(on: .home)) }
var timestamp: String?
if let lastMessageTimestamp = details.lastMessageTimestamp {
@@ -236,7 +236,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
hasUnreads: details.unreadNotificationCount > 0,
timestamp: timestamp,
lastMessage: details.lastMessage,
avatarURLString: details.avatarURLString,
avatarURL: details.avatarURL,
avatar: avatarImage)
}
@@ -61,13 +61,13 @@ enum RoomDetailsViewAction {
struct RoomDetailsMember: Identifiable, Equatable {
let id: String
let name: String?
let avatarUrl: String?
let avatarURL: URL?
// cached
var avatar: UIImage?
init(withProxy proxy: RoomMemberProxy) {
id = proxy.userId
name = proxy.displayName
avatarUrl = proxy.avatarUrl
avatarURL = proxy.avatarURL
}
}
@@ -54,8 +54,7 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
if let avatarURL = roomProxy.avatarURL {
Task {
if case let .success(avatar) = await mediaProvider.loadImageFromURLString(avatarURL,
avatarSize: .room(on: .details)) {
if case let .success(avatar) = await mediaProvider.loadImageFromURL(avatarURL, avatarSize: .room(on: .details)) {
state.roomAvatar = avatar
}
}
@@ -46,15 +46,14 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
return
}
if member.avatar != nil {
// already loaded
// Avatar already loaded.
return
}
guard let avatarUrl = member.avatarUrl else {
// user has no avatar
guard let avatarURL = member.avatarURL else {
return
}
switch await mediaProvider.loadImageFromURLString(avatarUrl, avatarSize: .user(on: .roomDetails)) {
switch await mediaProvider.loadImageFromURL(avatarURL, avatarSize: .user(on: .roomDetails)) {
case .success(let image):
if let index = state.members.firstIndex(where: { $0.id == memberId }) {
state.members[index].avatar = image
@@ -36,7 +36,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
timelineViewFactory: RoomTimelineViewFactoryProtocol,
mediaProvider: MediaProviderProtocol,
roomName: String?,
roomAvatarUrl: String? = nil) {
roomAvatarUrl: URL? = nil) {
self.timelineController = timelineController
self.timelineViewFactory = timelineViewFactory
self.mediaProvider = mediaProvider
@@ -76,11 +76,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.contextMenuBuilder = buildContexMenuForItemId(_:)
buildTimelineViews()
if let roomAvatarUrl {
Task {
if case let .success(avatar) = await mediaProvider.loadImageFromURLString(roomAvatarUrl,
avatarSize: .room(on: .timeline)) {
if case let .success(avatar) = await mediaProvider.loadImageFromURL(roomAvatarUrl,
avatarSize: .room(on: .timeline)) {
state.roomAvatar = avatar
}
}
@@ -261,7 +261,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
case .reply:
state.bindings.composerFocused = true
state.composerMode = .reply(id: item.id, displayName: item.senderDisplayName ?? item.senderId)
state.composerMode = .reply(id: item.id, displayName: item.sender.displayName ?? item.sender.id)
case .viewSource:
let debugDescription = timelineController.debugDescriptionFor(item.id)
MXLog.info(debugDescription)
@@ -71,7 +71,7 @@ struct RoomHeaderView_Previews: PreviewProvider {
timelineViewFactory: RoomTimelineViewFactory(),
mediaProvider: MockMediaProvider(),
roomName: "Some Room name",
roomAvatarUrl: "mock_url")
roomAvatarUrl: URL.picturesDirectory)
RoomHeaderView(context: viewModel.context)
.previewLayout(.sizeThatFits)
@@ -60,7 +60,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
HStack(alignment: .top, spacing: 4) {
TimelineSenderAvatarView(timelineItem: timelineItem)
.accessibilityHidden(true)
Text(timelineItem.senderDisplayName ?? timelineItem.senderId)
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
.font(.element.footnoteBold)
.foregroundColor(.element.primaryContent)
.lineLimit(1)
@@ -47,7 +47,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
if timelineItem.shouldShowSenderDetails {
HStack {
TimelineSenderAvatarView(timelineItem: timelineItem)
Text(timelineItem.senderDisplayName ?? timelineItem.senderId)
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
.font(.body)
.foregroundColor(.element.primaryContent)
.fontWeight(.semibold)
@@ -22,13 +22,10 @@ struct EmoteRoomTimelineView: View {
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
HStack(alignment: .top) {
Image(systemName: "face.dashed").padding(.top, 1.0)
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
} else {
FormattedBodyText(text: timelineItem.text)
}
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
} else {
FormattedBodyText(text: timelineItem.text)
}
}
}
@@ -59,6 +56,6 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: senderId)
sender: .init(id: senderId))
}
}
@@ -81,6 +81,6 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: isOutgoing,
isEditable: false,
senderId: senderId)
sender: .init(id: senderId))
}
}
@@ -47,7 +47,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
source: nil,
thumbnailSource: nil))
@@ -57,7 +57,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
source: nil,
thumbnailSource: nil))
@@ -67,7 +67,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
source: nil,
thumbnailSource: nil))
}
@@ -62,7 +62,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
source: nil,
image: UIImage(systemName: "photo")))
@@ -72,7 +72,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
source: nil,
image: nil))
@@ -82,7 +82,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
source: nil,
image: nil,
aspectRatio: 0.7,
@@ -59,6 +59,6 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: senderId)
sender: .init(id: senderId))
}
}
@@ -46,6 +46,6 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: senderId)
sender: .init(id: senderId))
}
}
@@ -63,8 +63,8 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
urlString: nil,
sender: .init(id: "Bob"),
imageURL: nil,
image: UIImage(systemName: "photo")))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
@@ -73,8 +73,8 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
urlString: nil,
sender: .init(id: "Bob"),
imageURL: nil,
image: nil))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
@@ -83,8 +83,8 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
urlString: nil,
sender: .init(id: "Bob"),
imageURL: nil,
image: nil,
aspectRatio: 0.7,
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
@@ -68,6 +68,6 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: isOutgoing,
isEditable: isOutgoing,
senderId: senderId)
sender: .init(id: senderId))
}
}
@@ -69,6 +69,6 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: isOutgoing,
isEditable: false,
senderId: senderId)
sender: .init(id: senderId))
}
}
@@ -72,7 +72,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
duration: 21,
source: nil,
thumbnailSource: nil,
@@ -84,7 +84,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
duration: 22,
source: nil,
thumbnailSource: nil,
@@ -96,7 +96,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
groupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
sender: .init(id: "Bob"),
duration: 23,
source: nil,
thumbnailSource: nil,
@@ -24,14 +24,14 @@ struct TimelineSenderAvatarView: View {
var body: some View {
ZStack(alignment: .center) {
if let avatar = timelineItem.senderAvatar {
if let avatar = timelineItem.sender.avatar {
Image(uiImage: avatar)
.resizable()
.scaledToFill()
.overlay(Circle().stroke(Color.element.accent))
} else {
PlaceholderAvatarImage(text: timelineItem.senderDisplayName ?? timelineItem.senderId,
contentId: timelineItem.senderId)
PlaceholderAvatarImage(text: timelineItem.sender.displayName ?? timelineItem.sender.id,
contentId: timelineItem.sender.id)
}
}
.clipShape(Circle())
@@ -41,6 +41,6 @@ struct TimelineSenderAvatarView: View {
.stroke(Color.element.background, lineWidth: 3)
)
.animation(.elementDefault, value: timelineItem.senderAvatar)
.animation(.elementDefault, value: timelineItem.sender.avatar)
}
}
@@ -27,22 +27,22 @@ class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
self.userSession = userSession
let bindings = SettingsViewStateBindings()
super.init(initialViewState: .init(bindings: bindings, deviceID: userSession.deviceId, userID: userSession.userID))
Task {
if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, avatarSize: .user(on: .settings)) {
if case let .success(userAvatarURL) = await userSession.clientProxy.loadUserAvatarURL() {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURL(userAvatarURL, avatarSize: .user(on: .settings)) {
state.userAvatar = avatar
}
}
}
Task {
if case let .success(userDisplayName) = await self.userSession.clientProxy.loadUserDisplayName() {
state.userDisplayName = userDisplayName
}
}
}
override func process(viewAction: SettingsViewAction) async {
switch viewAction {
case .close: