mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
DM Details have been updated (#2836)
This commit is contained in:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user