822 lines
31 KiB
Swift
822 lines
31 KiB
Swift
//
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalMessaging
|
|
import SignalUI
|
|
|
|
class ProfileSettingsViewController: OWSTableViewController2 {
|
|
|
|
private let context: ViewControllerContext = .shared
|
|
|
|
private var hasUnsavedChanges = false {
|
|
didSet { updateNavigationItem() }
|
|
}
|
|
|
|
private var avatarData: Data?
|
|
private var givenName: String?
|
|
private var familyName: String?
|
|
private var localUsernameState: Usernames.LocalUsernameState?
|
|
private var bio: String?
|
|
private var bioEmoji: String?
|
|
private var allBadges: [OWSUserProfileBadgeInfo] = []
|
|
private var displayBadgesOnProfile: Bool = false
|
|
|
|
private var shouldShowUsernameLinkTooltip: Bool = false
|
|
private var currentUsernameLinkTooltip: UsernameLinkTooltipView?
|
|
|
|
weak private var usernameChangeDelegate: UsernameChangeDelegate?
|
|
weak private var usernameLinkScanDelegate: UsernameLinkScanDelegate?
|
|
|
|
init(
|
|
usernameChangeDelegate: UsernameChangeDelegate,
|
|
usernameLinkScanDelegate: UsernameLinkScanDelegate
|
|
) {
|
|
self.usernameChangeDelegate = usernameChangeDelegate
|
|
self.usernameLinkScanDelegate = usernameLinkScanDelegate
|
|
|
|
super.init()
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
owsAssertDebug(navigationController != nil)
|
|
|
|
title = OWSLocalizedString("PROFILE_VIEW_TITLE", comment: "Title for the profile view.")
|
|
|
|
defaultSeparatorInsetLeading = Self.cellHInnerMargin + 24 + OWSTableItem.iconSpacing
|
|
|
|
let snapshot = profileManagerImpl.localProfileSnapshot(shouldIncludeAvatar: true)
|
|
avatarData = snapshot.avatarData
|
|
givenName = snapshot.givenName
|
|
familyName = snapshot.familyName
|
|
bio = snapshot.bio
|
|
bioEmoji = snapshot.bioEmoji
|
|
allBadges = snapshot.profileBadgeInfo ?? []
|
|
displayBadgesOnProfile = subscriptionManager.displayBadgesOnProfile
|
|
|
|
databaseStorage.read { tx -> Void in
|
|
localUsernameState = context.localUsernameManager
|
|
.usernameState(tx: tx.asV2Read)
|
|
shouldShowUsernameLinkTooltip = context.usernameEducationManager
|
|
.shouldShowUsernameLinkTooltip(tx: tx.asV2Read)
|
|
}
|
|
|
|
updateTableContents()
|
|
}
|
|
|
|
override var preferredNavigationBarStyle: OWSNavigationBarStyle {
|
|
return .blur
|
|
}
|
|
|
|
override var navbarBackgroundColorOverride: UIColor? {
|
|
return tableBackgroundColor
|
|
}
|
|
|
|
private var fullName: String? {
|
|
guard givenName?.isEmpty == false || familyName?.isEmpty == false else { return nil }
|
|
var nameComponents = PersonNameComponents()
|
|
nameComponents.givenName = givenName
|
|
nameComponents.familyName = familyName
|
|
return OWSFormat.formatNameComponents(nameComponents)
|
|
}
|
|
|
|
func updateTableContents() {
|
|
hideUsernameLinkTooltip(permanently: false)
|
|
|
|
let contents = OWSTableContents()
|
|
|
|
let avatarSection = OWSTableSection(items: [
|
|
OWSTableItem(customCellBlock: { [weak self] in
|
|
self?.avatarCell() ?? UITableViewCell()
|
|
}, actionBlock: nil),
|
|
OWSTableItem(customCellBlock: { [weak self] in
|
|
self?.createChangeAvatarCell() ?? UITableViewCell()
|
|
}, actionBlock: nil)
|
|
])
|
|
avatarSection.hasBackground = false
|
|
contents.add(avatarSection)
|
|
|
|
let mainSection = OWSTableSection()
|
|
mainSection.footerAttributedTitle = NSAttributedString.composed(of: [
|
|
OWSLocalizedString("PROFILE_VIEW_PROFILE_DESCRIPTION",
|
|
comment: "Description of the user profile."),
|
|
" ",
|
|
CommonStrings.learnMore.styled(
|
|
with: .link(URL(string: "https://support.signal.org/hc/articles/360007459591")!)
|
|
)
|
|
]).styled(
|
|
with: .font(.dynamicTypeCaption1Clamped),
|
|
.color(Theme.secondaryTextAndIconColor)
|
|
)
|
|
mainSection.add(.disclosureItem(
|
|
icon: .profileName,
|
|
name: fullName ?? OWSLocalizedString(
|
|
"PROFILE_SETTINGS_NAME_PLACEHOLDER",
|
|
comment: "Placeholder when the user doesn't have a 'name' defined for profile settings screen."
|
|
),
|
|
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "name"),
|
|
actionBlock: { [weak self] in
|
|
guard let self = self else { return }
|
|
let vc = ProfileNameViewController(givenName: self.givenName, familyName: self.familyName, profileDelegate: self)
|
|
self.presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
|
|
}
|
|
))
|
|
|
|
if FeatureFlags.usernames, let localUsernameState {
|
|
switch localUsernameState {
|
|
case .unset:
|
|
mainSection.add(usernameUnsetTableItem())
|
|
case let .available(username, usernameLink):
|
|
mainSection.add(usernameAvailableTableItem(username: username))
|
|
mainSection.add(usernameLinkAvailableTableItem(
|
|
username: username,
|
|
usernameLink: usernameLink
|
|
))
|
|
case let .linkCorrupted(username):
|
|
mainSection.add(usernameAvailableTableItem(username: username))
|
|
mainSection.add(usernameLinkCorruptedTableItem())
|
|
case .usernameAndLinkCorrupted:
|
|
mainSection.add(usernameCorruptedTableItem())
|
|
}
|
|
}
|
|
|
|
mainSection.add(.disclosureItem(
|
|
icon: .profileAbout,
|
|
name: OWSUserProfile.bioForDisplay(bio: bio, bioEmoji: bioEmoji) ?? OWSLocalizedString(
|
|
"PROFILE_SETTINGS_BIO_PLACEHOLDER",
|
|
comment: "Placeholder when the user doesn't have an 'about' for profile settings screen."
|
|
),
|
|
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "about"),
|
|
actionBlock: { [weak self] in
|
|
guard let self = self else { return }
|
|
let vc = ProfileBioViewController(bio: self.bio, bioEmoji: self.bioEmoji, profileDelegate: self)
|
|
self.presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
|
|
}
|
|
))
|
|
if RemoteConfig.donorBadgeDisplay, !allBadges.isEmpty {
|
|
mainSection.add(.disclosureItem(
|
|
icon: .profileBadges,
|
|
name: OWSLocalizedString(
|
|
"BADGE_CONFIGURATION_TITLE",
|
|
comment: "The title for the badge configuration page"
|
|
),
|
|
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "badges"),
|
|
actionBlock: { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
let avatarImage = self.databaseStorage.read { self.avatarImage(transaction: $0) }
|
|
|
|
let vc = BadgeConfigurationViewController(
|
|
availableBadges: self.allBadges,
|
|
shouldDisplayOnProfile: self.displayBadgesOnProfile,
|
|
avatarImage: avatarImage,
|
|
delegate: self)
|
|
self.presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
|
|
}
|
|
))
|
|
}
|
|
contents.add(mainSection)
|
|
|
|
self.contents = contents
|
|
}
|
|
|
|
@objc
|
|
func presentAvatarSettingsView() {
|
|
let currentAvatarImage: UIImage? = {
|
|
guard let avatarData = avatarData else { return nil }
|
|
return UIImage(data: avatarData)
|
|
}()
|
|
|
|
let vc = AvatarSettingsViewController(
|
|
context: .profile,
|
|
currentAvatarImage: currentAvatarImage
|
|
) { [weak self] newAvatarImage in
|
|
self?.setAvatarImage(newAvatarImage)
|
|
}
|
|
presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
|
|
}
|
|
|
|
// MARK: - Username
|
|
|
|
/// A table item for if there is no username set.
|
|
private func usernameUnsetTableItem() -> OWSTableItem {
|
|
return OWSTableItem(
|
|
customCellBlock: {
|
|
return OWSTableItem.buildCell(
|
|
icon: .profileUsername,
|
|
itemName: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_PLACEHOLDER",
|
|
comment: "A placeholder value shown in the profile settings screen on a tappable item leading to a username selection flow, for when the user doesn't have a username."
|
|
),
|
|
accessoryType: .disclosureIndicator
|
|
)
|
|
},
|
|
actionBlock: { [weak self] in
|
|
guard let self else { return }
|
|
self.presentUsernameSelection(currentUsername: nil)
|
|
}
|
|
)
|
|
}
|
|
|
|
/// A table item for an available username.
|
|
private func usernameAvailableTableItem(username: String) -> OWSTableItem {
|
|
// No action block required, as the cell will handle taps itself
|
|
// by presenting a context menu.
|
|
return OWSTableItem(
|
|
customCellBlock: { [weak self] in
|
|
let editUsernameAction = ContextMenuAction(
|
|
title: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_EDIT_USERNAME_ACTION",
|
|
comment: "Title for a menu action allowing users to edit their existing username."
|
|
),
|
|
image: Theme.iconImage(.contextMenuEdit),
|
|
handler: { [weak self] _ in
|
|
self?.presentUsernameSelection(currentUsername: username)
|
|
}
|
|
)
|
|
|
|
let deleteUsernameAction = ContextMenuAction(
|
|
title: CommonStrings.deleteButton,
|
|
image: Theme.iconImage(.contextMenuDelete),
|
|
attributes: .destructive,
|
|
handler: { [weak self] _ in
|
|
self?.offerToDeleteUsername(currentUsername: username)
|
|
}
|
|
)
|
|
|
|
let contextMenuButton = ContextMenuButton(
|
|
contextMenu: ContextMenu([
|
|
editUsernameAction,
|
|
deleteUsernameAction
|
|
]),
|
|
preferredContextMenuPosition: ContextMenuButton.ContextMenuPosition(
|
|
verticalPinnedEdge: .bottom,
|
|
horizontalPinnedEdge: CurrentAppContext().isRTL ? .right : .left,
|
|
alignmentOffset: CGPoint(
|
|
x: Self.cellHInnerMargin,
|
|
y: Self.cellVInnerMargin
|
|
)
|
|
)
|
|
)
|
|
|
|
contextMenuButton.showsContextMenuAsPrimaryAction = true
|
|
|
|
let contextMenuPresentingCell = ContextMenuPresentingTableViewCell(
|
|
contextMenuButton: contextMenuButton
|
|
)
|
|
|
|
return OWSTableItem.buildCell(
|
|
baseCell: contextMenuPresentingCell,
|
|
contentWrapperView: contextMenuButton,
|
|
icon: .profileUsername,
|
|
itemName: username,
|
|
accessoryType: .disclosureIndicator
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
/// A table item for an available username and username link.
|
|
private func usernameLinkAvailableTableItem(
|
|
username: String,
|
|
usernameLink: Usernames.UsernameLink
|
|
) -> OWSTableItem {
|
|
return OWSTableItem(
|
|
customCellBlock: {
|
|
return OWSTableItem.buildCell(
|
|
icon: .qrCode,
|
|
itemName: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_LINK_CELL_TITLE",
|
|
comment: "Title for a table cell that lets the user manage their username link and QR code."
|
|
),
|
|
accessoryType: .disclosureIndicator
|
|
)
|
|
},
|
|
willDisplayBlock: { [weak self] cell in
|
|
guard let self else { return }
|
|
|
|
self.hideUsernameLinkTooltip(permanently: false)
|
|
|
|
if self.shouldShowUsernameLinkTooltip {
|
|
self.currentUsernameLinkTooltip = UsernameLinkTooltipView(
|
|
fromView: self.view,
|
|
referenceView: cell,
|
|
hInsetFromReferenceView: self.cellPillFrame(view: cell).x + 16,
|
|
onDismiss: { [weak self] in
|
|
self?.hideUsernameLinkTooltip(permanently: true)
|
|
}
|
|
)
|
|
}
|
|
},
|
|
actionBlock: { [weak self] in
|
|
self?.presentUsernameLink(
|
|
username: username,
|
|
usernameLink: usernameLink
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
/// A table item for if the username is corrupted.
|
|
private func usernameCorruptedTableItem() -> OWSTableItem {
|
|
return OWSTableItem(
|
|
customCellBlock: { [weak self] in
|
|
return OWSTableItem.buildCell(
|
|
icon: .profileUsername,
|
|
itemName: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_PLACEHOLDER",
|
|
comment: "A placeholder value shown in the profile settings screen on a tappable item leading to a username selection flow, for when the user doesn't have a username."
|
|
),
|
|
accessoryType: .disclosureIndicator,
|
|
accessoryContentView: self?.buildUsernameErrorIconView()
|
|
)
|
|
},
|
|
actionBlock: { [weak self] in
|
|
self?.presentUsernameCorruptedResolution()
|
|
}
|
|
)
|
|
}
|
|
|
|
private func usernameLinkCorruptedTableItem() -> OWSTableItem {
|
|
return OWSTableItem(
|
|
customCellBlock: { [weak self] in
|
|
return OWSTableItem.buildCell(
|
|
icon: .qrCode,
|
|
itemName: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_LINK_CELL_TITLE",
|
|
comment: "Title for a table cell that lets the user manage their username link and QR code."
|
|
),
|
|
accessoryType: .disclosureIndicator,
|
|
accessoryContentView: self?.buildUsernameErrorIconView()
|
|
)
|
|
},
|
|
actionBlock: { [weak self] in
|
|
self?.presentUsernameLinkCorruptedResolution()
|
|
}
|
|
)
|
|
}
|
|
|
|
private func buildUsernameErrorIconView() -> UIView {
|
|
let imageView = UIImageView.withTemplateImageName(
|
|
"error-circle",
|
|
tintColor: .ows_accentRed
|
|
)
|
|
|
|
imageView.autoPinToSquareAspectRatio()
|
|
|
|
return imageView
|
|
}
|
|
|
|
// MARK: Username actions
|
|
|
|
public func presentUsernameCorruptedResolution() {
|
|
guard let localUsernameState else {
|
|
return
|
|
}
|
|
|
|
switch localUsernameState {
|
|
case .usernameAndLinkCorrupted:
|
|
break
|
|
case .unset, .available, .linkCorrupted:
|
|
owsFailDebug("Attempted to present username corrupted resolution, but username is not corrupted!")
|
|
return
|
|
}
|
|
|
|
let actionSheet = ActionSheetController(message: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_CORRUPTED_RESOLUTION_CONFIRMATION_ALERT_MESSAGE",
|
|
comment: "A message explaining that something is wrong with the username, on a sheet allowing the user to resolve the issue."
|
|
))
|
|
|
|
actionSheet.addAction(ActionSheetAction(
|
|
title: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_CORRUPTED_RESOLUTION_CREATE_NEW_USERNAME_ACTION_TITLE",
|
|
comment: "Title for an action sheet button allowing users to create a new username when their current one is corrupted."
|
|
),
|
|
handler: { [weak self] _ in
|
|
self?.presentUsernameSelection(currentUsername: nil)
|
|
}
|
|
))
|
|
|
|
actionSheet.addAction(ActionSheetAction(
|
|
title: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_CORRUPTED_RESOLUTION_DELETE_USERNAME_ACTION_TITLE",
|
|
comment: "Title for an action sheet button allowing users to delete their corrupted username."
|
|
),
|
|
style: .destructive,
|
|
handler: { [weak self] _ in
|
|
self?.deleteUsernameBehindModalActivityIndicator()
|
|
}
|
|
))
|
|
|
|
actionSheet.addAction(OWSActionSheets.cancelAction)
|
|
|
|
OWSActionSheets.showActionSheet(actionSheet, fromViewController: self)
|
|
}
|
|
|
|
public func presentUsernameLinkCorruptedResolution() {
|
|
guard let localUsernameState else {
|
|
return
|
|
}
|
|
|
|
switch localUsernameState {
|
|
case let .linkCorrupted(username):
|
|
presentUsernameLink(username: username, usernameLink: nil)
|
|
case .unset, .available, .usernameAndLinkCorrupted:
|
|
owsFailDebug("Attempted to present username link corrupted resolution, but username link is not corrupted!")
|
|
}
|
|
}
|
|
|
|
private func presentUsernameSelection(currentUsername: String?) {
|
|
let usernameSelectionCoordinator = UsernameSelectionCoordinator(
|
|
currentUsername: currentUsername,
|
|
usernameChangeDelegate: self,
|
|
context: .init(
|
|
databaseStorage: databaseStorage,
|
|
networkManager: networkManager,
|
|
schedulers: context.schedulers,
|
|
storageServiceManager: storageServiceManager,
|
|
usernameEducationManager: context.usernameEducationManager,
|
|
localUsernameManager: context.localUsernameManager
|
|
)
|
|
)
|
|
|
|
usernameSelectionCoordinator.present(fromViewController: self)
|
|
}
|
|
|
|
private func offerToDeleteUsername(currentUsername: String) {
|
|
OWSActionSheets.showConfirmationAlert(
|
|
message: String(
|
|
format: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_DELETION_CONFIRMATION_ALERT_MESSAGE_FORMAT",
|
|
comment: "A message asking the user if they are sure they want to remove their username and explaining what will happen. Embeds {{ the user's current username }}."
|
|
),
|
|
currentUsername
|
|
),
|
|
proceedTitle: OWSLocalizedString(
|
|
"PROFILE_SETTINGS_USERNAME_DELETION_USERNAME_ACTION_TITLE",
|
|
comment: "The title of an action sheet button that will delete a user's username."
|
|
),
|
|
proceedStyle: .destructive
|
|
) { [weak self] _ in
|
|
guard let self else { return }
|
|
|
|
self.deleteUsernameBehindModalActivityIndicator()
|
|
}
|
|
}
|
|
|
|
private func deleteUsernameBehindModalActivityIndicator() {
|
|
ModalActivityIndicatorViewController.present(
|
|
fromViewController: self,
|
|
canCancel: false
|
|
) { modal in
|
|
firstly(on: self.context.schedulers.global()) { () -> Promise<Void> in
|
|
self.context.db.write { tx in
|
|
return self.context.localUsernameManager
|
|
.deleteUsername(tx: tx)
|
|
}
|
|
}
|
|
.ensure(on: self.context.schedulers.main) {
|
|
let newState = self.context.db.read { tx in
|
|
return self.context.localUsernameManager.usernameState(tx: tx)
|
|
}
|
|
|
|
// State may have changed with either success or failure.
|
|
self.usernameStateDidChange(newState: newState)
|
|
}
|
|
.done(on: self.context.schedulers.main) {
|
|
modal.dismiss()
|
|
}
|
|
.catch(on: self.context.schedulers.main) { error in
|
|
modal.dismiss {
|
|
OWSActionSheets.showErrorAlert(
|
|
message: CommonStrings.somethingWentWrongTryAgainLaterError
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func presentUsernameLink(
|
|
username: String,
|
|
usernameLink: Usernames.UsernameLink?
|
|
) {
|
|
presentFormSheet(
|
|
OWSNavigationController(
|
|
rootViewController: UsernameLinkQRCodeContentController(
|
|
db: DependenciesBridge.shared.db,
|
|
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
|
|
schedulers: DependenciesBridge.shared.schedulers,
|
|
username: username,
|
|
usernameLink: usernameLink,
|
|
changeDelegate: self,
|
|
scanDelegate: self
|
|
)
|
|
),
|
|
animated: true
|
|
) {
|
|
self.hideUsernameLinkTooltip(permanently: true)
|
|
}
|
|
}
|
|
|
|
private func hideUsernameLinkTooltip(permanently: Bool) {
|
|
if let currentUsernameLinkTooltip {
|
|
currentUsernameLinkTooltip.removeFromSuperview()
|
|
self.currentUsernameLinkTooltip = nil
|
|
}
|
|
|
|
if permanently {
|
|
shouldShowUsernameLinkTooltip = false
|
|
|
|
databaseStorage.write { tx in
|
|
context.usernameEducationManager
|
|
.setShouldShowUsernameLinkTooltip(false, tx: tx.asV2Write)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Event Handling
|
|
|
|
override func themeDidChange() {
|
|
super.themeDidChange()
|
|
updateTableContents()
|
|
}
|
|
|
|
override func contentSizeCategoryDidChange() {
|
|
super.contentSizeCategoryDidChange()
|
|
updateTableContents()
|
|
}
|
|
|
|
private func leaveViewCheckingForUnsavedChanges() {
|
|
if !hasUnsavedChanges {
|
|
// If user made no changes, return to conversation settings view.
|
|
profileCompleted()
|
|
return
|
|
}
|
|
|
|
OWSActionSheets.showPendingChangesActionSheet(discardAction: { [weak self] in
|
|
self?.profileCompleted()
|
|
})
|
|
}
|
|
|
|
private func updateNavigationItem() {
|
|
if hasUnsavedChanges {
|
|
// If we have a unsaved changes, right item should be a "save" button.
|
|
let saveButton = UIBarButtonItem(
|
|
barButtonSystemItem: .save,
|
|
target: self,
|
|
action: #selector(updateProfile),
|
|
accessibilityIdentifier: "save_button"
|
|
)
|
|
navigationItem.rightBarButtonItem = saveButton
|
|
} else {
|
|
navigationItem.rightBarButtonItem = nil
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func updateProfile() {
|
|
|
|
let normalizedGivenName = self.normalizedGivenName
|
|
let normalizedFamilyName = self.normalizedFamilyName
|
|
let normalizedBio = self.normalizedBio
|
|
let normalizedBioEmoji = self.normalizedBioEmoji
|
|
let visibleBadgeIds = displayBadgesOnProfile ? self.allBadges.map { $0.badgeId } : []
|
|
let displayBadgesOnProfile = displayBadgesOnProfile
|
|
|
|
if !self.reachabilityManager.isReachable {
|
|
OWSActionSheets.showErrorAlert(message: OWSLocalizedString("PROFILE_VIEW_NO_CONNECTION",
|
|
comment: "Error shown when the user tries to update their profile when the app is not connected to the internet."))
|
|
return
|
|
}
|
|
|
|
// Show an activity indicator to block the UI during the profile upload.
|
|
let avatarData = self.avatarData
|
|
ModalActivityIndicatorViewController.present(fromViewController: self,
|
|
canCancel: false) { modalActivityIndicator in
|
|
firstly(on: DispatchQueue.global()) { () -> Promise<Void> in
|
|
OWSProfileManager.updateLocalProfilePromise(
|
|
profileGivenName: normalizedGivenName,
|
|
profileFamilyName: normalizedFamilyName,
|
|
profileBio: normalizedBio,
|
|
profileBioEmoji: normalizedBioEmoji,
|
|
profileAvatarData: avatarData,
|
|
visibleBadgeIds: visibleBadgeIds,
|
|
userProfileWriter: .localUser
|
|
)
|
|
}.then(on: DispatchQueue.global()) { () -> Promise<Void> in
|
|
Self.databaseStorage.writePromise { transaction in
|
|
Self.subscriptionManager.setDisplayBadgesOnProfile(
|
|
displayBadgesOnProfile,
|
|
updateStorageService: true,
|
|
transaction: transaction
|
|
)
|
|
}.asVoid()
|
|
}.done(on: DispatchQueue.main) { _ in
|
|
modalActivityIndicator.dismiss { [weak self] in
|
|
AssertIsOnMainThread()
|
|
self?.profileCompleted()
|
|
}
|
|
}.catch(on: DispatchQueue.main) { error in
|
|
owsFailDebug("Error: \(error)")
|
|
modalActivityIndicator.dismiss { [weak self] in
|
|
AssertIsOnMainThread()
|
|
self?.profileCompleted()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var normalizedGivenName: String {
|
|
(givenName ?? "").ows_stripped()
|
|
}
|
|
|
|
private var normalizedFamilyName: String {
|
|
(familyName ?? "").ows_stripped()
|
|
}
|
|
|
|
private var normalizedBio: String? {
|
|
bio?.ows_stripped()
|
|
}
|
|
|
|
private var normalizedBioEmoji: String? {
|
|
bioEmoji?.ows_stripped()
|
|
}
|
|
|
|
private func profileCompleted() {
|
|
AssertIsOnMainThread()
|
|
Logger.verbose("")
|
|
|
|
navigationController?.popViewController(animated: true)
|
|
}
|
|
|
|
// MARK: - Avatar
|
|
|
|
private let avatarSizeClass: ConversationAvatarView.Configuration.SizeClass = .eightyEight
|
|
|
|
private func avatarImage(transaction: SDSAnyReadTransaction) -> UIImage? {
|
|
if let avatarData = avatarData {
|
|
return UIImage(data: avatarData)
|
|
} else {
|
|
return avatarBuilder.defaultAvatarImageForLocalUser(
|
|
diameterPoints: avatarSizeClass.diameter,
|
|
transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private func avatarCell() -> UITableViewCell {
|
|
let cell = OWSTableItem.newCell()
|
|
|
|
cell.selectionStyle = .none
|
|
|
|
let sizeClass = ConversationAvatarView.Configuration.SizeClass.eightyEight
|
|
let badgedAvatarView = ConversationAvatarView(sizeClass: sizeClass, localUserDisplayMode: .asUser)
|
|
databaseStorage.read { readTx in
|
|
let primaryBadge = allBadges.first
|
|
let badgeAssets = primaryBadge?.badge?.assets
|
|
let badgeImage = badgeAssets.flatMap { sizeClass.fetchImageFromBadgeAssets($0) }
|
|
|
|
badgedAvatarView.update(readTx) { config in
|
|
config.dataSource = .asset(
|
|
avatar: self.avatarImage(transaction: readTx),
|
|
badge: self.displayBadgesOnProfile ? badgeImage : nil
|
|
)
|
|
}
|
|
}
|
|
|
|
cell.contentView.addSubview(badgedAvatarView)
|
|
badgedAvatarView.autoPinHeightToSuperviewMargins()
|
|
badgedAvatarView.autoHCenterInSuperview()
|
|
return cell
|
|
}
|
|
|
|
private func createChangeAvatarCell() -> UITableViewCell {
|
|
let cell = OWSTableItem.newCell()
|
|
cell.selectionStyle = .none
|
|
|
|
let changeButton = UIButton(type: .custom)
|
|
|
|
changeButton.setTitle(OWSLocalizedString("CHANGE_AVATAR_BUTTON_LABEL", comment: "Button label to allow user to change avatar"), for: .normal)
|
|
changeButton.titleLabel?.font = .dynamicTypeBody2.semibold()
|
|
changeButton.contentEdgeInsets = UIEdgeInsets(hMargin: 16, vMargin: 6)
|
|
changeButton.layer.cornerRadius = 16
|
|
|
|
changeButton.setTitleColor(Theme.isDarkThemeEnabled ? .ows_gray05 : .ows_gray95, for: .normal)
|
|
changeButton.backgroundColor = self.cellBackgroundColor
|
|
|
|
cell.contentView.addSubview(changeButton)
|
|
changeButton.autoPinHeightToSuperviewMargins()
|
|
changeButton.autoPinWidthToSuperviewMargins(relation: .lessThanOrEqual)
|
|
changeButton.autoCenterInSuperview()
|
|
changeButton.setContentHuggingHigh()
|
|
|
|
changeButton.addTarget(self, action: #selector(presentAvatarSettingsView), for: .touchUpInside)
|
|
return cell
|
|
}
|
|
|
|
private func setAvatarImage(_ avatarImage: UIImage?) {
|
|
AssertIsOnMainThread()
|
|
|
|
var avatarData: Data?
|
|
if let avatarImage = avatarImage {
|
|
avatarData = OWSProfileManager.avatarData(forAvatarImage: avatarImage)
|
|
}
|
|
hasUnsavedChanges = hasUnsavedChanges || avatarData != self.avatarData
|
|
self.avatarData = avatarData
|
|
|
|
updateTableContents()
|
|
}
|
|
}
|
|
|
|
extension ProfileSettingsViewController {
|
|
|
|
override public var isModalInPresentation: Bool {
|
|
get { hasUnsavedChanges }
|
|
set { /* noop superclass requirement */ }
|
|
}
|
|
|
|
public var shouldCancelNavigationBack: Bool {
|
|
let result = hasUnsavedChanges
|
|
if result {
|
|
leaveViewCheckingForUnsavedChanges()
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
extension ProfileSettingsViewController: ProfileBioViewControllerDelegate {
|
|
|
|
public func profileBioViewDidComplete(bio: String?, bioEmoji: String?) {
|
|
hasUnsavedChanges = hasUnsavedChanges || bio != self.bio || bioEmoji != self.bioEmoji
|
|
|
|
self.bio = bio
|
|
self.bioEmoji = bioEmoji
|
|
|
|
updateTableContents()
|
|
}
|
|
}
|
|
|
|
extension ProfileSettingsViewController: ProfileNameViewControllerDelegate {
|
|
func profileNameViewDidComplete(givenName: String?, familyName: String?) {
|
|
hasUnsavedChanges = hasUnsavedChanges || givenName != self.givenName || familyName != self.familyName
|
|
|
|
self.givenName = givenName
|
|
self.familyName = familyName
|
|
|
|
updateTableContents()
|
|
}
|
|
}
|
|
|
|
extension ProfileSettingsViewController: BadgeConfigurationDelegate {
|
|
func badgeConfiguration(_ vc: BadgeConfigurationViewController, didCompleteWithBadgeSetting setting: BadgeConfiguration) {
|
|
switch setting {
|
|
case .doNotDisplayPublicly:
|
|
if displayBadgesOnProfile {
|
|
Logger.info("Configured to disable public badge visibility")
|
|
hasUnsavedChanges = true
|
|
displayBadgesOnProfile = false
|
|
updateTableContents()
|
|
}
|
|
case .display(featuredBadge: let newFeaturedBadge):
|
|
guard allBadges.contains(where: { $0.badgeId == newFeaturedBadge.badgeId }) else {
|
|
owsFailDebug("Invalid badge")
|
|
return
|
|
}
|
|
|
|
if !displayBadgesOnProfile || newFeaturedBadge.badgeId != allBadges.first?.badgeId {
|
|
Logger.info("Configured to show badges publicly featuring: \(newFeaturedBadge.badgeId)")
|
|
hasUnsavedChanges = true
|
|
displayBadgesOnProfile = true
|
|
|
|
let nonPrimaryBadges = allBadges.filter { $0.badgeId != newFeaturedBadge.badgeId }
|
|
allBadges = [newFeaturedBadge] + nonPrimaryBadges
|
|
updateTableContents()
|
|
}
|
|
}
|
|
|
|
vc.dismiss(animated: true)
|
|
}
|
|
|
|
func badgeConfirmationDidCancel(_ vc: BadgeConfigurationViewController) {
|
|
vc.dismiss(animated: true)
|
|
}
|
|
}
|
|
|
|
extension ProfileSettingsViewController: UsernameChangeDelegate {
|
|
func usernameStateDidChange(newState: Usernames.LocalUsernameState) {
|
|
localUsernameState = newState
|
|
|
|
updateTableContents()
|
|
|
|
usernameChangeDelegate?.usernameStateDidChange(newState: newState)
|
|
}
|
|
}
|
|
|
|
extension ProfileSettingsViewController: UsernameLinkScanDelegate {
|
|
func usernameLinkScanned(_ usernameLink: Usernames.UsernameLink) {
|
|
usernameLinkScanDelegate?.usernameLinkScanned(usernameLink)
|
|
}
|
|
}
|