Compare commits
4 Commits
430f21bf62
...
a66eb04489
| Author | SHA1 | Date | |
|---|---|---|---|
| a66eb04489 | |||
| feed384cf1 | |||
| 5601f475c8 | |||
| 4e24f468b8 |
@ -676,9 +676,13 @@ struct MessageProfileView: View {
|
|||||||
formatted
|
formatted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var shouldShowRelationshipQuickActions: Bool {
|
private var shouldShowRelationshipQuickActions: Bool {
|
||||||
|
if isDeletedUser { return false }
|
||||||
|
|
||||||
guard let relationship = currentChatProfile?.relationship else { return false }
|
guard let relationship = currentChatProfile?.relationship else { return false }
|
||||||
|
|
||||||
|
// показываем только если НЕ в контактах
|
||||||
return !relationship.isTargetInContactsOfCurrentUser
|
return !relationship.isTargetInContactsOfCurrentUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,13 @@ struct ContactsTab: View {
|
|||||||
@State private var creatingChatForContactId: UUID?
|
@State private var creatingChatForContactId: UUID?
|
||||||
@State private var pendingChatItem: PrivateChatListItem?
|
@State private var pendingChatItem: PrivateChatListItem?
|
||||||
@State private var isPendingChatActive = false
|
@State private var isPendingChatActive = false
|
||||||
|
@State private var contactAvatars: [UUID: AvatarInfo] = [:]
|
||||||
|
@State private var avatarLoadedIds: Set<UUID> = []
|
||||||
|
@State private var avatarLoadingIds: Set<UUID> = []
|
||||||
|
|
||||||
private let contactsService = ContactsService()
|
private let contactsService = ContactsService()
|
||||||
private let chatService = ChatService()
|
private let chatService = ChatService()
|
||||||
|
private let profileService = ProfileService()
|
||||||
private let pageSize = 25
|
private let pageSize = 25
|
||||||
|
|
||||||
private var currentUserId: String? {
|
private var currentUserId: String? {
|
||||||
@ -42,11 +46,17 @@ struct ContactsTab: View {
|
|||||||
Button {
|
Button {
|
||||||
openChat(for: contact)
|
openChat(for: contact)
|
||||||
} label: {
|
} label: {
|
||||||
ContactRow(contact: contact, isLoading: creatingChatForContactId == contact.id)
|
ContactRow(
|
||||||
|
contact: contact,
|
||||||
|
avatarInfo: contactAvatars[contact.id],
|
||||||
|
currentUserId: currentUserId,
|
||||||
|
isLoading: creatingChatForContactId == contact.id
|
||||||
|
)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.disabled(contact.isDeleted || creatingChatForContactId == contact.id)
|
.disabled(creatingChatForContactId == contact.id)
|
||||||
|
// .disabled(contact.isDeleted || creatingChatForContactId == contact.id)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
handleContactAction(.edit, for: contact)
|
handleContactAction(.edit, for: contact)
|
||||||
@ -80,6 +90,7 @@ struct ContactsTab: View {
|
|||||||
}
|
}
|
||||||
.listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12))
|
.listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
loadAvatarIfNeeded(for: contact)
|
||||||
if index >= contacts.count - 5 {
|
if index >= contacts.count - 5 {
|
||||||
Task {
|
Task {
|
||||||
await loadContacts(reset: false)
|
await loadContacts(reset: false)
|
||||||
@ -101,9 +112,6 @@ struct ContactsTab: View {
|
|||||||
.task {
|
.task {
|
||||||
await loadContacts(reset: false)
|
await loadContacts(reset: false)
|
||||||
}
|
}
|
||||||
.refreshable {
|
|
||||||
await refreshContacts()
|
|
||||||
}
|
|
||||||
.alert(item: $activeAlert) { alert in
|
.alert(item: $activeAlert) { alert in
|
||||||
switch alert {
|
switch alert {
|
||||||
case .error(_, let message):
|
case .error(_, let message):
|
||||||
@ -270,7 +278,6 @@ struct ContactsTab: View {
|
|||||||
|
|
||||||
private func openChat(for contact: Contact) {
|
private func openChat(for contact: Contact) {
|
||||||
guard creatingChatForContactId == nil else { return }
|
guard creatingChatForContactId == nil else { return }
|
||||||
guard !contact.isDeleted else { return }
|
|
||||||
|
|
||||||
creatingChatForContactId = contact.id
|
creatingChatForContactId = contact.id
|
||||||
|
|
||||||
@ -344,35 +351,56 @@ struct ContactsTab: View {
|
|||||||
|
|
||||||
return NSLocalizedString("Произошла неизвестная ошибка. Попробуйте позже.", comment: "Chat creation unknown error")
|
return NSLocalizedString("Произошла неизвестная ошибка. Попробуйте позже.", comment: "Chat creation unknown error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadAvatarIfNeeded(for contact: Contact) {
|
||||||
|
guard !contact.isDeleted else { return }
|
||||||
|
let contactId = contact.id
|
||||||
|
if avatarLoadedIds.contains(contactId) || avatarLoadingIds.contains(contactId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarLoadingIds.insert(contactId)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let profile = try await profileService.fetchProfile(userId: contactId)
|
||||||
|
await MainActor.run {
|
||||||
|
if let info = profile.avatars?.current {
|
||||||
|
contactAvatars[contactId] = info
|
||||||
|
}
|
||||||
|
avatarLoadedIds.insert(contactId)
|
||||||
|
avatarLoadingIds.remove(contactId)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if AppConfig.DEBUG {
|
||||||
|
print("[ContactsTab] load avatar failed for \(contactId): \(error)")
|
||||||
|
}
|
||||||
|
await MainActor.run {
|
||||||
|
avatarLoadingIds.remove(contactId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ContactRow: View {
|
private struct ContactRow: View {
|
||||||
let contact: Contact
|
let contact: Contact
|
||||||
|
let avatarInfo: AvatarInfo?
|
||||||
|
let currentUserId: String?
|
||||||
let isLoading: Bool
|
let isLoading: Bool
|
||||||
|
|
||||||
init(contact: Contact, isLoading: Bool = false) {
|
private let avatarSize: CGFloat = 40
|
||||||
|
|
||||||
|
init(contact: Contact, avatarInfo: AvatarInfo? = nil, currentUserId: String? = nil, isLoading: Bool = false) {
|
||||||
self.contact = contact
|
self.contact = contact
|
||||||
|
self.avatarInfo = avatarInfo
|
||||||
|
self.currentUserId = currentUserId
|
||||||
self.isLoading = isLoading
|
self.isLoading = isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top, spacing: 10) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
Circle()
|
avatarView
|
||||||
.fill(contact.isDeleted ? Color(.systemGray5) : Color.accentColor.opacity(0.15))
|
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
.overlay(
|
|
||||||
Group {
|
|
||||||
if contact.isDeleted {
|
|
||||||
Image(systemName: "person.slash")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(Color(.systemGray2))
|
|
||||||
} else {
|
|
||||||
Text(contact.initials)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
HStack(alignment: .firstTextBaseline) {
|
HStack(alignment: .firstTextBaseline) {
|
||||||
@ -414,6 +442,56 @@ private struct ContactRow: View {
|
|||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var avatarView: some View {
|
||||||
|
if let fileId = avatarInfo?.fileId,
|
||||||
|
let url = avatarURL(for: fileId),
|
||||||
|
let currentUserId {
|
||||||
|
CachedAvatarView(url: url, fileId: fileId, userId: currentUserId) {
|
||||||
|
placeholderAvatar
|
||||||
|
}
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: avatarSize, height: avatarSize)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else {
|
||||||
|
placeholderAvatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func avatarURL(for fileId: String) -> URL? {
|
||||||
|
let userId = contact.id.uuidString
|
||||||
|
let path = "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(userId)?file_id=\(fileId)"
|
||||||
|
return URL(string: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var placeholderAvatar: some View {
|
||||||
|
Circle()
|
||||||
|
.fill(avatarBackgroundColor)
|
||||||
|
.frame(width: avatarSize, height: avatarSize)
|
||||||
|
.overlay(
|
||||||
|
Group {
|
||||||
|
if contact.isDeleted {
|
||||||
|
Image(systemName: "person.slash")
|
||||||
|
.symbolRenderingMode(.hierarchical)
|
||||||
|
.font(.system(size: avatarSize * 0.45, weight: .semibold))
|
||||||
|
.foregroundColor(avatarTextColor)
|
||||||
|
} else {
|
||||||
|
Text(contact.initials)
|
||||||
|
.font(.system(size: avatarSize * 0.5, weight: .semibold))
|
||||||
|
.foregroundColor(avatarTextColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var avatarBackgroundColor: Color {
|
||||||
|
contact.isDeleted ? Color(.systemGray5) : Color.accentColor.opacity(0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var avatarTextColor: Color {
|
||||||
|
contact.isDeleted ? Color.accentColor : Color.accentColor
|
||||||
|
}
|
||||||
|
|
||||||
private var friendCodeBadge: some View {
|
private var friendCodeBadge: some View {
|
||||||
Text(NSLocalizedString("Код дружбы", comment: "Friend code badge"))
|
Text(NSLocalizedString("Код дружбы", comment: "Friend code badge"))
|
||||||
.font(.caption2.weight(.medium))
|
.font(.caption2.weight(.medium))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user