DM Details have been updated (#2836)

This commit is contained in:
Mauro
2024-05-14 18:47:20 +02:00
committed by GitHub
parent d66ff02d12
commit 2c038c5969
68 changed files with 314 additions and 169 deletions
+5 -2
View File
@@ -46,6 +46,7 @@ enum UserAvatarSizeOnScreen {
case home
case settings
case roomDetails
case dmDetails
case startChat
case memberDetails
case inviteUsers
@@ -76,11 +77,13 @@ enum UserAvatarSizeOnScreen {
case .startChat:
return 36
case .memberDetails:
return 70
return 96
case .inviteUsers:
return 56
case .editUserDetails:
return 96
case .dmDetails:
return 75
}
}
}
@@ -110,7 +113,7 @@ enum RoomAvatarSizeOnScreen {
case .home:
return 52
case .details:
return 70
return 96
case .joinRoom:
return 96
}
@@ -0,0 +1,37 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
extension View {
func scaledOffset(x: CGFloat = 0, y: CGFloat = 0, relativeTo textStyle: Font.TextStyle = .body) -> some View {
modifier(ScaledOffsetModifier(x: x, y: y, relativeTo: textStyle))
}
}
private struct ScaledOffsetModifier: ViewModifier {
@ScaledMetric var x: CGFloat
@ScaledMetric var y: CGFloat
init(x: CGFloat, y: CGFloat, relativeTo textStyle: Font.TextStyle) {
_x = ScaledMetric(wrappedValue: x, relativeTo: textStyle)
_y = ScaledMetric(wrappedValue: y, relativeTo: textStyle)
}
func body(content: Content) -> some View {
content.offset(x: x, y: y)
}
}
@@ -17,31 +17,53 @@
import SwiftUI
struct AvatarHeaderView<Footer: View>: View {
private struct AvatarInfo {
let id: String
let name: String?
let avatarURL: URL?
init(from room: RoomDetails) {
id = room.id
name = room.name
avatarURL = room.avatarURL
}
init(from member: RoomMemberDetails) {
id = member.id
name = member.isBanned ? nil : member.name
avatarURL = member.isBanned ? nil : member.avatarURL
}
init(from user: UserProfileProxy) {
id = user.userID
name = user.displayName
avatarURL = user.avatarURL
}
}
private enum Badge: Hashable {
case encrypted(Bool)
case `public`
}
let id: String
let name: String?
let subtitle: String?
let avatarURL: URL?
private let mainAvatarInfo: AvatarInfo
private let secondaryAvatarInfo: AvatarInfo?
private let subtitle: String?
private let badges: [Badge]
let avatarSize: AvatarSize
let imageProvider: ImageProviderProtocol?
var onAvatarTap: (() -> Void)?
@ViewBuilder var footer: () -> Footer
private let avatarSize: AvatarSize
private let imageProvider: ImageProviderProtocol?
private var onAvatarTap: (() -> Void)?
@ViewBuilder private var footer: () -> Footer
init(room: RoomDetails,
avatarSize: AvatarSize,
imageProvider: ImageProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
id = room.id
name = room.name
mainAvatarInfo = .init(from: room)
secondaryAvatarInfo = nil
subtitle = room.canonicalAlias
avatarURL = room.avatarURL
self.avatarSize = avatarSize
self.imageProvider = imageProvider
@@ -56,15 +78,31 @@ struct AvatarHeaderView<Footer: View>: View {
self.badges = badges
}
init(accountOwner: RoomMemberDetails,
dmRecipient: RoomMemberDetails,
imageProvider: ImageProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
mainAvatarInfo = .init(from: dmRecipient)
secondaryAvatarInfo = .init(from: accountOwner)
subtitle = dmRecipient.isBanned ? nil : dmRecipient.name == nil ? nil : dmRecipient.id
avatarSize = .user(on: .dmDetails)
self.imageProvider = imageProvider
self.onAvatarTap = onAvatarTap
self.footer = footer
// In EL-X a DM is by definition always encrypted
badges = [.encrypted(true)]
}
init(member: RoomMemberDetails,
avatarSize: AvatarSize,
imageProvider: ImageProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
id = member.id
name = member.isBanned ? nil : member.name
mainAvatarInfo = .init(from: member)
secondaryAvatarInfo = nil
subtitle = member.isBanned ? nil : member.name == nil ? nil : member.id
avatarURL = member.isBanned ? nil : member.avatarURL
self.avatarSize = avatarSize
self.imageProvider = imageProvider
@@ -78,10 +116,9 @@ struct AvatarHeaderView<Footer: View>: View {
imageProvider: ImageProviderProtocol? = nil,
onAvatarTap: (() -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
id = user.userID
name = user.displayName
mainAvatarInfo = .init(from: user)
secondaryAvatarInfo = nil
subtitle = user.displayName == nil ? nil : user.userID
avatarURL = user.avatarURL
self.avatarSize = avatarSize
self.imageProvider = imageProvider
@@ -112,26 +149,66 @@ struct AvatarHeaderView<Footer: View>: View {
}
}
}
@ViewBuilder
private var avatar: some View {
if let secondaryAvatarInfo {
ZStack {
LoadableAvatarImage(url: mainAvatarInfo.avatarURL,
name: mainAvatarInfo.name,
contentID: mainAvatarInfo.id,
avatarSize: avatarSize,
imageProvider: imageProvider)
.scaledFrame(size: 120, alignment: .topTrailing)
LoadableAvatarImage(url: secondaryAvatarInfo.avatarURL,
name: secondaryAvatarInfo.name,
contentID: secondaryAvatarInfo.id,
avatarSize: avatarSize,
imageProvider: imageProvider)
.mask {
Rectangle()
.fill(Color.white)
.overlay {
Circle()
.inset(by: -4)
.fill(Color.black)
.scaledOffset(x: 120 - avatarSize.value,
y: -120 + avatarSize.value)
}
.compositingGroup()
.luminanceToAlpha()
}
.scaledFrame(size: 120, alignment: .bottomLeading)
}
.scaledFrame(size: 120)
} else {
LoadableAvatarImage(url: mainAvatarInfo.avatarURL,
name: mainAvatarInfo.name,
contentID: mainAvatarInfo.id,
avatarSize: avatarSize,
imageProvider: imageProvider)
}
}
var body: some View {
VStack(spacing: 8.0) {
Button {
onAvatarTap?()
} label: {
LoadableAvatarImage(url: avatarURL,
name: name,
contentID: id,
avatarSize: avatarSize,
imageProvider: imageProvider)
avatar
}
.buttonStyle(.borderless) // Add a button style to stop the whole row being tappable.
Text(name ?? id)
Spacer()
.frame(height: 9)
Text(mainAvatarInfo.name ?? mainAvatarInfo.id)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingLGBold)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
.textSelection(.enabled)
if let subtitle {
Text(subtitle)
.foregroundColor(.compound.textSecondary)
@@ -177,6 +254,20 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
}
.previewDisplayName("Room")
Form {
AvatarHeaderView(accountOwner: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockMe), dmRecipient: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice),
imageProvider: MockMediaProvider()) {
HStack(spacing: 32) {
ShareLink(item: "test") {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: "Test"))
}
.padding(.top, 32)
}
}
.previewDisplayName("DM")
VStack(spacing: 16) {
AvatarHeaderView(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice),
avatarSize: .room(on: .details),
@@ -64,6 +64,7 @@ struct RoomDetailsScreenViewState: BindableState {
var bindings: RoomDetailsScreenViewStateBindings
var dmRecipient: RoomMemberDetails?
var accountOwner: RoomMemberDetails?
var shortcuts: [RoomDetailsScreenViewShortcut] {
var shortcuts: [RoomDetailsScreenViewShortcut] = [.mute]
@@ -174,9 +174,11 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.receive(on: DispatchQueue.main)
.sink { [weak self, ownUserID = roomProxy.ownUserID] members in
guard let self else { return }
let accountOwner = members.first(where: { $0.userID == ownUserID })
let dmRecipient = members.first(where: { $0.userID != ownUserID })
self.dmRecipient = dmRecipient
self.state.dmRecipient = dmRecipient.map(RoomMemberDetails.init(withProxy:))
self.state.accountOwner = accountOwner.map(RoomMemberDetails.init(withProxy:))
}
.store(in: &cancellables)
@@ -24,8 +24,10 @@ struct RoomDetailsScreen: View {
var body: some View {
Form {
if let recipient = context.viewState.dmRecipient {
dmHeaderSection(recipient: recipient)
if let recipient = context.viewState.dmRecipient,
let accountOwner = context.viewState.accountOwner {
dmHeaderSection(accountOwner: accountOwner,
recipient: recipient)
} else {
normalRoomHeaderSection
}
@@ -82,9 +84,9 @@ struct RoomDetailsScreen: View {
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.avatar)
}
private func dmHeaderSection(recipient: RoomMemberDetails) -> some View {
AvatarHeaderView(member: recipient,
avatarSize: .user(on: .memberDetails),
private func dmHeaderSection(accountOwner: RoomMemberDetails, recipient: RoomMemberDetails) -> some View {
AvatarHeaderView(accountOwner: accountOwner,
dmRecipient: recipient,
imageProvider: context.imageProvider) {
context.send(viewAction: .displayAvatar)
} footer: {