mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Allow focusing the different avatars making up a DM details cluster separately. (#3341)
This commit is contained in:
@@ -26,13 +26,13 @@ struct AvatarHeaderView<Footer: View>: View {
|
|||||||
|
|
||||||
private let avatarSize: AvatarSize
|
private let avatarSize: AvatarSize
|
||||||
private let mediaProvider: MediaProviderProtocol?
|
private let mediaProvider: MediaProviderProtocol?
|
||||||
private var onAvatarTap: (() -> Void)?
|
private var onAvatarTap: ((URL) -> Void)?
|
||||||
@ViewBuilder private var footer: () -> Footer
|
@ViewBuilder private var footer: () -> Footer
|
||||||
|
|
||||||
init(room: RoomDetails,
|
init(room: RoomDetails,
|
||||||
avatarSize: AvatarSize,
|
avatarSize: AvatarSize,
|
||||||
mediaProvider: MediaProviderProtocol? = nil,
|
mediaProvider: MediaProviderProtocol? = nil,
|
||||||
onAvatarTap: (() -> Void)? = nil,
|
onAvatarTap: ((URL) -> Void)? = nil,
|
||||||
@ViewBuilder footer: @escaping () -> Footer) {
|
@ViewBuilder footer: @escaping () -> Footer) {
|
||||||
avatarInfo = .room(room.avatar)
|
avatarInfo = .room(room.avatar)
|
||||||
title = room.name ?? room.id
|
title = room.name ?? room.id
|
||||||
@@ -54,7 +54,7 @@ struct AvatarHeaderView<Footer: View>: View {
|
|||||||
init(accountOwner: RoomMemberDetails,
|
init(accountOwner: RoomMemberDetails,
|
||||||
dmRecipient: RoomMemberDetails,
|
dmRecipient: RoomMemberDetails,
|
||||||
mediaProvider: MediaProviderProtocol? = nil,
|
mediaProvider: MediaProviderProtocol? = nil,
|
||||||
onAvatarTap: (() -> Void)? = nil,
|
onAvatarTap: ((URL) -> Void)? = nil,
|
||||||
@ViewBuilder footer: @escaping () -> Footer) {
|
@ViewBuilder footer: @escaping () -> Footer) {
|
||||||
let dmRecipientProfile = UserProfileProxy(member: dmRecipient)
|
let dmRecipientProfile = UserProfileProxy(member: dmRecipient)
|
||||||
avatarInfo = .room(.heroes([dmRecipientProfile, UserProfileProxy(member: accountOwner)]))
|
avatarInfo = .room(.heroes([dmRecipientProfile, UserProfileProxy(member: accountOwner)]))
|
||||||
@@ -72,7 +72,7 @@ struct AvatarHeaderView<Footer: View>: View {
|
|||||||
init(member: RoomMemberDetails,
|
init(member: RoomMemberDetails,
|
||||||
avatarSize: AvatarSize,
|
avatarSize: AvatarSize,
|
||||||
mediaProvider: MediaProviderProtocol? = nil,
|
mediaProvider: MediaProviderProtocol? = nil,
|
||||||
onAvatarTap: (() -> Void)? = nil,
|
onAvatarTap: ((URL) -> Void)? = nil,
|
||||||
@ViewBuilder footer: @escaping () -> Footer) {
|
@ViewBuilder footer: @escaping () -> Footer) {
|
||||||
let profile = UserProfileProxy(member: member)
|
let profile = UserProfileProxy(member: member)
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ struct AvatarHeaderView<Footer: View>: View {
|
|||||||
init(user: UserProfileProxy,
|
init(user: UserProfileProxy,
|
||||||
avatarSize: AvatarSize,
|
avatarSize: AvatarSize,
|
||||||
mediaProvider: MediaProviderProtocol? = nil,
|
mediaProvider: MediaProviderProtocol? = nil,
|
||||||
onAvatarTap: (() -> Void)? = nil,
|
onAvatarTap: ((URL) -> Void)? = nil,
|
||||||
@ViewBuilder footer: @escaping () -> Footer) {
|
@ViewBuilder footer: @escaping () -> Footer) {
|
||||||
avatarInfo = .user(user)
|
avatarInfo = .user(user)
|
||||||
title = user.displayName ?? user.userID
|
title = user.displayName ?? user.userID
|
||||||
@@ -128,24 +128,22 @@ struct AvatarHeaderView<Footer: View>: View {
|
|||||||
case .room(let roomAvatar):
|
case .room(let roomAvatar):
|
||||||
RoomAvatarImage(avatar: roomAvatar,
|
RoomAvatarImage(avatar: roomAvatar,
|
||||||
avatarSize: avatarSize,
|
avatarSize: avatarSize,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider,
|
||||||
|
onAvatarTap: onAvatarTap)
|
||||||
|
|
||||||
case .user(let userProfile):
|
case .user(let userProfile):
|
||||||
LoadableAvatarImage(url: userProfile.avatarURL,
|
LoadableAvatarImage(url: userProfile.avatarURL,
|
||||||
name: userProfile.displayName,
|
name: userProfile.displayName,
|
||||||
contentID: userProfile.userID,
|
contentID: userProfile.userID,
|
||||||
avatarSize: avatarSize,
|
avatarSize: avatarSize,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider,
|
||||||
|
onTap: onAvatarTap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 8.0) {
|
VStack(spacing: 8.0) {
|
||||||
Button {
|
avatar
|
||||||
onAvatarTap?()
|
|
||||||
} label: {
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless) // Add a button style to stop the whole row being tappable.
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 9)
|
.frame(height: 9)
|
||||||
|
|||||||
@@ -13,20 +13,39 @@ struct LoadableAvatarImage: View {
|
|||||||
private let contentID: String?
|
private let contentID: String?
|
||||||
private let avatarSize: AvatarSize
|
private let avatarSize: AvatarSize
|
||||||
private let mediaProvider: MediaProviderProtocol?
|
private let mediaProvider: MediaProviderProtocol?
|
||||||
|
private let onTap: ((URL) -> Void)?
|
||||||
|
|
||||||
@ScaledMetric private var frameSize: CGFloat
|
@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.url = url
|
||||||
self.name = name
|
self.name = name
|
||||||
self.contentID = contentID
|
self.contentID = contentID
|
||||||
self.avatarSize = avatarSize
|
self.avatarSize = avatarSize
|
||||||
self.mediaProvider = mediaProvider
|
self.mediaProvider = mediaProvider
|
||||||
|
self.onTap = onTap
|
||||||
|
|
||||||
_frameSize = ScaledMetric(wrappedValue: avatarSize.value)
|
_frameSize = ScaledMetric(wrappedValue: avatarSize.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
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
|
avatar
|
||||||
.frame(width: frameSize, height: frameSize)
|
.frame(width: frameSize, height: frameSize)
|
||||||
.background(Color.compound.bgCanvasDefault)
|
.background(Color.compound.bgCanvasDefault)
|
||||||
@@ -34,7 +53,7 @@ struct LoadableAvatarImage: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var avatar: some View {
|
private var avatar: some View {
|
||||||
if let url {
|
if let url {
|
||||||
LoadableImage(url: url,
|
LoadableImage(url: url,
|
||||||
mediaType: .avatar,
|
mediaType: .avatar,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ struct RoomAvatarImage: View {
|
|||||||
let avatarSize: AvatarSize
|
let avatarSize: AvatarSize
|
||||||
let mediaProvider: MediaProviderProtocol?
|
let mediaProvider: MediaProviderProtocol?
|
||||||
|
|
||||||
|
private(set) var onAvatarTap: ((URL) -> Void)?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch avatar {
|
switch avatar {
|
||||||
case .room(let id, let name, let avatarURL):
|
case .room(let id, let name, let avatarURL):
|
||||||
@@ -32,7 +34,8 @@ struct RoomAvatarImage: View {
|
|||||||
name: name,
|
name: name,
|
||||||
contentID: id,
|
contentID: id,
|
||||||
avatarSize: avatarSize,
|
avatarSize: avatarSize,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider,
|
||||||
|
onTap: onAvatarTap)
|
||||||
case .heroes(let users):
|
case .heroes(let users):
|
||||||
// We will expand upon this with more stack sizes in the future.
|
// We will expand upon this with more stack sizes in the future.
|
||||||
if users.count == 0 {
|
if users.count == 0 {
|
||||||
@@ -45,14 +48,16 @@ struct RoomAvatarImage: View {
|
|||||||
name: users[0].displayName,
|
name: users[0].displayName,
|
||||||
contentID: users[0].userID,
|
contentID: users[0].userID,
|
||||||
avatarSize: avatarSize,
|
avatarSize: avatarSize,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider,
|
||||||
|
onTap: onAvatarTap)
|
||||||
.scaledFrame(size: clusterSize, alignment: .topTrailing)
|
.scaledFrame(size: clusterSize, alignment: .topTrailing)
|
||||||
|
|
||||||
LoadableAvatarImage(url: users[1].avatarURL,
|
LoadableAvatarImage(url: users[1].avatarURL,
|
||||||
name: users[1].displayName,
|
name: users[1].displayName,
|
||||||
contentID: users[1].userID,
|
contentID: users[1].userID,
|
||||||
avatarSize: avatarSize,
|
avatarSize: avatarSize,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider,
|
||||||
|
onTap: onAvatarTap)
|
||||||
.mask {
|
.mask {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.white)
|
.fill(Color.white)
|
||||||
@@ -74,7 +79,8 @@ struct RoomAvatarImage: View {
|
|||||||
name: users[0].displayName,
|
name: users[0].displayName,
|
||||||
contentID: users[0].userID,
|
contentID: users[0].userID,
|
||||||
avatarSize: avatarSize,
|
avatarSize: avatarSize,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider,
|
||||||
|
onTap: onAvatarTap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ enum RoomDetailsScreenViewAction {
|
|||||||
case unignoreConfirmed
|
case unignoreConfirmed
|
||||||
case processTapNotifications
|
case processTapNotifications
|
||||||
case processToggleMuteNotifications
|
case processToggleMuteNotifications
|
||||||
case displayAvatar
|
case displayAvatar(URL)
|
||||||
case processTapPolls
|
case processTapPolls
|
||||||
case toggleFavourite(isFavourite: Bool)
|
case toggleFavourite(isFavourite: Bool)
|
||||||
case processTapRolesAndPermissions
|
case processTapRolesAndPermissions
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
}
|
}
|
||||||
case .processToggleMuteNotifications:
|
case .processToggleMuteNotifications:
|
||||||
Task { await toggleMuteNotifications() }
|
Task { await toggleMuteNotifications() }
|
||||||
case .displayAvatar:
|
case .displayAvatar(let url):
|
||||||
displayFullScreenAvatar()
|
displayFullScreenAvatar(url)
|
||||||
case .processTapPolls:
|
case .processTapPolls:
|
||||||
actionsSubject.send(.requestPollsHistoryPresentation)
|
actionsSubject.send(.requestPollsHistoryPresentation)
|
||||||
case .toggleFavourite(let isFavourite):
|
case .toggleFavourite(let isFavourite):
|
||||||
@@ -346,11 +346,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func displayFullScreenAvatar() {
|
private func displayFullScreenAvatar(_ url: URL) {
|
||||||
guard let avatarURL = roomProxy.avatarURL else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadingIndicatorIdentifier = "roomAvatarLoadingIndicator"
|
let loadingIndicatorIdentifier = "roomAvatarLoadingIndicator"
|
||||||
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
|
|
||||||
@@ -360,7 +356,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We don't actually know the mime type here, assume it's an image.
|
// 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)
|
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.roomTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ struct RoomDetailsScreen: View {
|
|||||||
private var normalRoomHeaderSection: some View {
|
private var normalRoomHeaderSection: some View {
|
||||||
AvatarHeaderView(room: context.viewState.details,
|
AvatarHeaderView(room: context.viewState.details,
|
||||||
avatarSize: .room(on: .details),
|
avatarSize: .room(on: .details),
|
||||||
mediaProvider: context.mediaProvider) {
|
mediaProvider: context.mediaProvider) { url in
|
||||||
context.send(viewAction: .displayAvatar)
|
context.send(viewAction: .displayAvatar(url))
|
||||||
} footer: {
|
} footer: {
|
||||||
if !context.viewState.shortcuts.isEmpty {
|
if !context.viewState.shortcuts.isEmpty {
|
||||||
headerSectionShortcuts
|
headerSectionShortcuts
|
||||||
@@ -78,8 +78,8 @@ struct RoomDetailsScreen: View {
|
|||||||
private func dmHeaderSection(accountOwner: RoomMemberDetails, recipient: RoomMemberDetails) -> some View {
|
private func dmHeaderSection(accountOwner: RoomMemberDetails, recipient: RoomMemberDetails) -> some View {
|
||||||
AvatarHeaderView(accountOwner: accountOwner,
|
AvatarHeaderView(accountOwner: accountOwner,
|
||||||
dmRecipient: recipient,
|
dmRecipient: recipient,
|
||||||
mediaProvider: context.mediaProvider) {
|
mediaProvider: context.mediaProvider) { url in
|
||||||
context.send(viewAction: .displayAvatar)
|
context.send(viewAction: .displayAvatar(url))
|
||||||
} footer: {
|
} footer: {
|
||||||
if !context.viewState.shortcuts.isEmpty {
|
if !context.viewState.shortcuts.isEmpty {
|
||||||
headerSectionShortcuts
|
headerSectionShortcuts
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ enum RoomMemberDetailsScreenViewAction {
|
|||||||
case showIgnoreAlert
|
case showIgnoreAlert
|
||||||
case ignoreConfirmed
|
case ignoreConfirmed
|
||||||
case unignoreConfirmed
|
case unignoreConfirmed
|
||||||
case displayAvatar
|
case displayAvatar(URL)
|
||||||
case openDirectChat
|
case openDirectChat
|
||||||
case startCall(roomID: String)
|
case startCall(roomID: String)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-8
@@ -84,8 +84,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
|||||||
Task { await ignoreUser() }
|
Task { await ignoreUser() }
|
||||||
case .unignoreConfirmed:
|
case .unignoreConfirmed:
|
||||||
Task { await unignoreUser() }
|
Task { await unignoreUser() }
|
||||||
case .displayAvatar:
|
case .displayAvatar(let url):
|
||||||
Task { await displayFullScreenAvatar() }
|
Task { await displayFullScreenAvatar(url) }
|
||||||
case .openDirectChat:
|
case .openDirectChat:
|
||||||
Task { await openDirectChat() }
|
Task { await openDirectChat() }
|
||||||
case .startCall(let roomID):
|
case .startCall(let roomID):
|
||||||
@@ -143,21 +143,17 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func displayFullScreenAvatar() async {
|
private func displayFullScreenAvatar(_ url: URL) async {
|
||||||
guard let roomMemberProxy else {
|
guard let roomMemberProxy else {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let avatarURL = roomMemberProxy.avatarURL else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator"
|
let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator"
|
||||||
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) }
|
defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) }
|
||||||
|
|
||||||
// We don't actually know the mime type here, assume it's an image.
|
// 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)
|
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ struct RoomMemberDetailsScreen: View {
|
|||||||
if let memberDetails = context.viewState.memberDetails {
|
if let memberDetails = context.viewState.memberDetails {
|
||||||
AvatarHeaderView(member: memberDetails,
|
AvatarHeaderView(member: memberDetails,
|
||||||
avatarSize: .user(on: .memberDetails),
|
avatarSize: .user(on: .memberDetails),
|
||||||
mediaProvider: context.mediaProvider) {
|
mediaProvider: context.mediaProvider) { url in
|
||||||
context.send(viewAction: .displayAvatar)
|
context.send(viewAction: .displayAvatar(url))
|
||||||
} footer: {
|
} footer: {
|
||||||
otherUserFooter
|
otherUserFooter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ struct UserProfileScreenViewStateBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum UserProfileScreenViewAction {
|
enum UserProfileScreenViewAction {
|
||||||
case displayAvatar
|
case displayAvatar(URL)
|
||||||
case openDirectChat
|
case openDirectChat
|
||||||
case startCall(roomID: String)
|
case startCall(roomID: String)
|
||||||
case dismiss
|
case dismiss
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
|||||||
|
|
||||||
override func process(viewAction: UserProfileScreenViewAction) {
|
override func process(viewAction: UserProfileScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .displayAvatar:
|
case .displayAvatar(let url):
|
||||||
Task { await displayFullScreenAvatar() }
|
Task { await displayFullScreenAvatar(url) }
|
||||||
case .openDirectChat:
|
case .openDirectChat:
|
||||||
Task { await openDirectChat() }
|
Task { await openDirectChat() }
|
||||||
case .startCall(let roomID):
|
case .startCall(let roomID):
|
||||||
@@ -87,15 +87,14 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func displayFullScreenAvatar() async {
|
private func displayFullScreenAvatar(_ url: URL) async {
|
||||||
guard let userProfile = state.userProfile else { fatalError() }
|
guard let userProfile = state.userProfile else { fatalError() }
|
||||||
guard let avatarURL = userProfile.avatarURL else { return }
|
|
||||||
|
|
||||||
showLoadingIndicator(allowsInteraction: false)
|
showLoadingIndicator(allowsInteraction: false)
|
||||||
defer { hideLoadingIndicator() }
|
defer { hideLoadingIndicator() }
|
||||||
|
|
||||||
// We don't actually know the mime type here, assume it's an image.
|
// 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)
|
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ struct UserProfileScreen: View {
|
|||||||
if let userProfile = context.viewState.userProfile {
|
if let userProfile = context.viewState.userProfile {
|
||||||
AvatarHeaderView(user: userProfile,
|
AvatarHeaderView(user: userProfile,
|
||||||
avatarSize: .user(on: .memberDetails),
|
avatarSize: .user(on: .memberDetails),
|
||||||
mediaProvider: context.mediaProvider) {
|
mediaProvider: context.mediaProvider) { url in
|
||||||
context.send(viewAction: .displayAvatar)
|
context.send(viewAction: .displayAvatar(url))
|
||||||
} footer: {
|
} footer: {
|
||||||
otherUserFooter
|
otherUserFooter
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user