SimpleX-Chat/apps/ios/Shared/Views/ChatList/UserPicker.swift
Stanislav Dmitrenko 153f80fe64
ios: menu to switch active user profile (#1758)
* ios: User chooser UI

* Change

* Changes

* update view

* fix layout/refactor

* fix preview

* wider menu, update label

* hide Your profiles button

* Clickable background that hides userChooser

* No click listener

* Better animation

* Disabled scrolling for small number of items

* Separated scrollview and buttons

* No transition

* Re-indent

* Limiting width by the longest label

* UserManagerView

* Adapted API

* Hide user chooser after selection

* Top counter,  users refactor

* Padding

* use VStack to fix layout bug

* eol

* rename: rename to getUserChatData

* update layout

* s/semibold/medium

* remove SettingsButton view

* rename

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-17 17:47:37 +00:00

181 lines
7.4 KiB
Swift

//
// Created by Avently on 16.01.2023.
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255))
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
struct UserPicker: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@Binding var showSettings: Bool
@Binding var userPickerVisible: Bool
var manageUsers: () -> Void = {}
@State var scrollViewContentSize: CGSize = .zero
@State var disableScrolling: Bool = true
private let menuButtonHeight: CGFloat = 68
@State var chatViewNameWidth: CGFloat = 0
var fillColor: Color {
colorScheme == .dark ? fillColorDark : fillColorLight
}
var body: some View {
VStack {
Spacer().frame(height: 1)
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 0) {
ForEach(Array(chatModel.users.enumerated()), id: \.0) { i, userInfo in
Button(action: {
if !userInfo.user.activeUser {
changeActiveUser(toUser: userInfo)
}
}, label: {
HStack(spacing: 0) {
ProfileImage(imageStr: userInfo.user.image)
.frame(width: 44, height: 44)
.padding(.trailing, 12)
Text(userInfo.user.chatViewName)
.fontWeight(i == 0 ? .medium : .regular)
.foregroundColor(.primary)
.overlay(DetermineWidth())
Spacer()
if i == 0 {
Image(systemName: "checkmark")
} else if userInfo.unreadCount > 0 {
unreadCounter(userInfo.unreadCount)
}
}
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
if i < chatModel.users.count - 1 {
Divider()
}
}
}
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
scrollViewContentSize = geo.size
let layoutFrame = UIApplication.shared.windows[0].safeAreaLayoutGuide.layoutFrame
disableScrolling = scrollViewContentSize.height + menuButtonHeight * 2 + 10 < layoutFrame.height
}
return Color.clear
}
}
}
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
.frame(maxHeight: scrollViewContentSize.height)
Divider()
menuButton("Your user profiles", icon: "pencil") {
manageUsers()
}
Divider()
menuButton("Settings", icon: "gearshape") {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.background(
Rectangle()
.fill(fillColor)
.cornerRadius(16)
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
)
.onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
.frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
.padding(8)
.onChange(of: [chatModel.currentUser?.chatViewName, chatModel.currentUser?.image] ) { _ in
reloadCurrentUser()
}
.opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear {
reloadUsers()
}
}
private func reloadCurrentUser() {
if let updatedUser = chatModel.currentUser, let index = chatModel.users.firstIndex(where: { $0.user.userId == updatedUser.userId }) {
let removed = chatModel.users.remove(at: index)
chatModel.users.insert(UserInfo(user: updatedUser, unreadCount: removed.unreadCount), at: 0)
}
}
private func changeActiveUser(toUser: UserInfo) {
Task {
do {
let activeUser = try apiSetActiveUser(toUser.user.userId)
let oldActiveIndex = chatModel.users.firstIndex(where: { $0.user.userId == chatModel.currentUser?.userId })!
var oldActive = chatModel.users[oldActiveIndex]
oldActive.user.activeUser = false
chatModel.users[oldActiveIndex] = oldActive
chatModel.currentUser = activeUser
let currentActiveIndex = chatModel.users.firstIndex(where: { $0.user.userId == activeUser.userId })!
let removed = chatModel.users.remove(at: currentActiveIndex)
chatModel.users.insert(UserInfo(user: activeUser, unreadCount: removed.unreadCount), at: 0)
chatModel.users = chatModel.users.map { $0 }
try getUserChatData(chatModel)
userPickerVisible = false
} catch {
logger.error("Unable to set active user: \(error.localizedDescription)")
}
}
}
private func reloadUsers() {
Task {
chatModel.users = listUsers().sorted { one, two -> Bool in one.user.activeUser }
}
}
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 0) {
Text(title)
.overlay(DetermineWidth())
Spacer()
Image(systemName: icon)
// .frame(width: 24, alignment: .center)
}
.padding(.horizontal)
.padding(.vertical, 22)
.frame(height: menuButtonHeight)
}
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
}
}
func unreadCounter(_ unread: Int64) -> some View {
unreadCountText(Int(truncatingIfNeeded: unread))
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(Color.accentColor)
.cornerRadius(10)
}
struct UserPicker_Previews: PreviewProvider {
static var previews: some View {
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker(
showSettings: Binding.constant(false),
userPickerVisible: Binding.constant(true)
)
.environmentObject(m)
}
}