Skip to content

Commit

Permalink
Allow focusing the different avatars making up a DM details cluster s…
Browse files Browse the repository at this point in the history
…eparately. (#3341)
  • Loading branch information
stefanceriu authored Sep 27, 2024
1 parent 9d23dec commit 8a39940
Show file tree
Hide file tree
Showing 12 changed files with 65 additions and 51 deletions.
24 changes: 11 additions & 13 deletions ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ struct AvatarHeaderView<Footer: View>: View {

private let avatarSize: AvatarSize
private let mediaProvider: MediaProviderProtocol?
private var onAvatarTap: (() -> Void)?
private var onAvatarTap: ((URL) -> Void)?
@ViewBuilder private var footer: () -> Footer

init(room: RoomDetails,
avatarSize: AvatarSize,
mediaProvider: MediaProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
onAvatarTap: ((URL) -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
avatarInfo = .room(room.avatar)
title = room.name ?? room.id
Expand All @@ -54,7 +54,7 @@ struct AvatarHeaderView<Footer: View>: View {
init(accountOwner: RoomMemberDetails,
dmRecipient: RoomMemberDetails,
mediaProvider: MediaProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
onAvatarTap: ((URL) -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
let dmRecipientProfile = UserProfileProxy(member: dmRecipient)
avatarInfo = .room(.heroes([dmRecipientProfile, UserProfileProxy(member: accountOwner)]))
Expand All @@ -72,7 +72,7 @@ struct AvatarHeaderView<Footer: View>: View {
init(member: RoomMemberDetails,
avatarSize: AvatarSize,
mediaProvider: MediaProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
onAvatarTap: ((URL) -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
let profile = UserProfileProxy(member: member)

Expand All @@ -86,7 +86,7 @@ struct AvatarHeaderView<Footer: View>: View {
init(user: UserProfileProxy,
avatarSize: AvatarSize,
mediaProvider: MediaProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
onAvatarTap: ((URL) -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
avatarInfo = .user(user)
title = user.displayName ?? user.userID
Expand Down Expand Up @@ -128,24 +128,22 @@ struct AvatarHeaderView<Footer: View>: View {
case .room(let roomAvatar):
RoomAvatarImage(avatar: roomAvatar,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
mediaProvider: mediaProvider,
onAvatarTap: onAvatarTap)

case .user(let userProfile):
LoadableAvatarImage(url: userProfile.avatarURL,
name: userProfile.displayName,
contentID: userProfile.userID,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
mediaProvider: mediaProvider,
onTap: onAvatarTap)
}
}

var body: some View {
VStack(spacing: 8.0) {
Button {
onAvatarTap?()
} label: {
avatar
}
.buttonStyle(.borderless) // Add a button style to stop the whole row being tappable.
avatar

Spacer()
.frame(height: 9)
Expand Down
23 changes: 21 additions & 2 deletions ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,47 @@ struct LoadableAvatarImage: View {
private let contentID: String?
private let avatarSize: AvatarSize
private let mediaProvider: MediaProviderProtocol?
private let onTap: ((URL) -> Void)?

@ScaledMetric private var frameSize: CGFloat

init(url: URL?, name: String?, contentID: String?, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol?) {
init(url: URL?, name: String?,
contentID: String?,
avatarSize: AvatarSize,
mediaProvider: MediaProviderProtocol?,
onTap: ((URL) -> Void)? = nil) {
self.url = url
self.name = name
self.contentID = contentID
self.avatarSize = avatarSize
self.mediaProvider = mediaProvider
self.onTap = onTap

_frameSize = ScaledMetric(wrappedValue: avatarSize.value)
}

var body: some View {
if let onTap, let url {
Button {
onTap(url)
} label: {
clippedAvatar
}
.buttonStyle(.borderless) // Add a button style to stop the whole row being tappable.
} else {
clippedAvatar
}
}

private var clippedAvatar: some View {
avatar
.frame(width: frameSize, height: frameSize)
.background(Color.compound.bgCanvasDefault)
.clipShape(Circle())
}

@ViewBuilder
var avatar: some View {
private var avatar: some View {
if let url {
LoadableImage(url: url,
mediaType: .avatar,
Expand Down
14 changes: 10 additions & 4 deletions ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ struct RoomAvatarImage: View {
let avatarSize: AvatarSize
let mediaProvider: MediaProviderProtocol?

private(set) var onAvatarTap: ((URL) -> Void)?

var body: some View {
switch avatar {
case .room(let id, let name, let avatarURL):
LoadableAvatarImage(url: avatarURL,
name: name,
contentID: id,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
mediaProvider: mediaProvider,
onTap: onAvatarTap)
case .heroes(let users):
// We will expand upon this with more stack sizes in the future.
if users.count == 0 {
Expand All @@ -45,14 +48,16 @@ struct RoomAvatarImage: View {
name: users[0].displayName,
contentID: users[0].userID,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
mediaProvider: mediaProvider,
onTap: onAvatarTap)
.scaledFrame(size: clusterSize, alignment: .topTrailing)

LoadableAvatarImage(url: users[1].avatarURL,
name: users[1].displayName,
contentID: users[1].userID,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
mediaProvider: mediaProvider,
onTap: onAvatarTap)
.mask {
Rectangle()
.fill(Color.white)
Expand All @@ -74,7 +79,8 @@ struct RoomAvatarImage: View {
name: users[0].displayName,
contentID: users[0].userID,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
mediaProvider: mediaProvider,
onTap: onAvatarTap)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ enum RoomDetailsScreenViewAction {
case unignoreConfirmed
case processTapNotifications
case processToggleMuteNotifications
case displayAvatar
case displayAvatar(URL)
case processTapPolls
case toggleFavourite(isFavourite: Bool)
case processTapRolesAndPermissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
case .processToggleMuteNotifications:
Task { await toggleMuteNotifications() }
case .displayAvatar:
displayFullScreenAvatar()
case .displayAvatar(let url):
displayFullScreenAvatar(url)
case .processTapPolls:
actionsSubject.send(.requestPollsHistoryPresentation)
case .toggleFavourite(let isFavourite):
Expand Down Expand Up @@ -346,11 +346,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
}

private func displayFullScreenAvatar() {
guard let avatarURL = roomProxy.avatarURL else {
return
}

private func displayFullScreenAvatar(_ url: URL) {
let loadingIndicatorIdentifier = "roomAvatarLoadingIndicator"
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))

Expand All @@ -360,7 +356,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}

// We don't actually know the mime type here, assume it's an image.
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) {
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) {
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.roomTitle)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ struct RoomDetailsScreen: View {
private var normalRoomHeaderSection: some View {
AvatarHeaderView(room: context.viewState.details,
avatarSize: .room(on: .details),
mediaProvider: context.mediaProvider) {
context.send(viewAction: .displayAvatar)
mediaProvider: context.mediaProvider) { url in
context.send(viewAction: .displayAvatar(url))
} footer: {
if !context.viewState.shortcuts.isEmpty {
headerSectionShortcuts
Expand All @@ -78,8 +78,8 @@ struct RoomDetailsScreen: View {
private func dmHeaderSection(accountOwner: RoomMemberDetails, recipient: RoomMemberDetails) -> some View {
AvatarHeaderView(accountOwner: accountOwner,
dmRecipient: recipient,
mediaProvider: context.mediaProvider) {
context.send(viewAction: .displayAvatar)
mediaProvider: context.mediaProvider) { url in
context.send(viewAction: .displayAvatar(url))
} footer: {
if !context.viewState.shortcuts.isEmpty {
headerSectionShortcuts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ enum RoomMemberDetailsScreenViewAction {
case showIgnoreAlert
case ignoreConfirmed
case unignoreConfirmed
case displayAvatar
case displayAvatar(URL)
case openDirectChat
case startCall(roomID: String)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
Task { await ignoreUser() }
case .unignoreConfirmed:
Task { await unignoreUser() }
case .displayAvatar:
Task { await displayFullScreenAvatar() }
case .displayAvatar(let url):
Task { await displayFullScreenAvatar(url) }
case .openDirectChat:
Task { await openDirectChat() }
case .startCall(let roomID):
Expand Down Expand Up @@ -143,21 +143,17 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
}
}

private func displayFullScreenAvatar() async {
private func displayFullScreenAvatar(_ url: URL) async {
guard let roomMemberProxy else {
fatalError()
}

guard let avatarURL = roomMemberProxy.avatarURL else {
return
}

let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator"
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) }

// We don't actually know the mime type here, assume it's an image.
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) {
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) {
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ struct RoomMemberDetailsScreen: View {
if let memberDetails = context.viewState.memberDetails {
AvatarHeaderView(member: memberDetails,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
context.send(viewAction: .displayAvatar)
mediaProvider: context.mediaProvider) { url in
context.send(viewAction: .displayAvatar(url))
} footer: {
otherUserFooter
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct UserProfileScreenViewStateBindings {
}

enum UserProfileScreenViewAction {
case displayAvatar
case displayAvatar(URL)
case openDirectChat
case startCall(roomID: String)
case dismiss
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr

override func process(viewAction: UserProfileScreenViewAction) {
switch viewAction {
case .displayAvatar:
Task { await displayFullScreenAvatar() }
case .displayAvatar(let url):
Task { await displayFullScreenAvatar(url) }
case .openDirectChat:
Task { await openDirectChat() }
case .startCall(let roomID):
Expand All @@ -87,15 +87,14 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr

// MARK: - Private

private func displayFullScreenAvatar() async {
private func displayFullScreenAvatar(_ url: URL) async {
guard let userProfile = state.userProfile else { fatalError() }
guard let avatarURL = userProfile.avatarURL else { return }

showLoadingIndicator(allowsInteraction: false)
defer { hideLoadingIndicator() }

// We don't actually know the mime type here, assume it's an image.
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) {
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) {
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ struct UserProfileScreen: View {
if let userProfile = context.viewState.userProfile {
AvatarHeaderView(user: userProfile,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
context.send(viewAction: .displayAvatar)
mediaProvider: context.mediaProvider) { url in
context.send(viewAction: .displayAvatar(url))
} footer: {
otherUserFooter
}
Expand Down

0 comments on commit 8a39940

Please sign in to comment.