molecular
This commit is contained in:
parent
b01a34365b
commit
ea50b838d2
@ -318,6 +318,7 @@
|
||||
D224799B231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */; };
|
||||
D22D8393241C27B100D3DF69 /* TemplateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22D8392241C27B100D3DF69 /* TemplateModel.swift */; };
|
||||
D22D8395241FB41200D3DF69 /* UIStackView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22D8394241FB41200D3DF69 /* UIStackView+Extension.swift */; };
|
||||
D23118B325124E18001C8440 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23118B225124E18001C8440 /* Notification.swift */; };
|
||||
D2351C7A24A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2351C7924A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift */; };
|
||||
D2351C7C24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2351C7B24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift */; };
|
||||
D236E5B4241FEB1000C38625 /* ListTwoColumnPriceDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236E5B2241FEB1000C38625 /* ListTwoColumnPriceDescription.swift */; };
|
||||
@ -481,6 +482,9 @@
|
||||
D2E2A99F23E07F8A000B42E6 /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E2A99E23E07F8A000B42E6 /* PillButton.swift */; };
|
||||
D2E2A9A123E095AB000B42E6 /* ButtonModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E2A9A023E095AB000B42E6 /* ButtonModelProtocol.swift */; };
|
||||
D2E2A9A323E096B1000B42E6 /* DisableableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E2A9A223E096B1000B42E6 /* DisableableModelProtocol.swift */; };
|
||||
D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D12513EA6900564112 /* NotificationXButton.swift */; };
|
||||
D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D32514F80C00564112 /* CollapsableNotification.swift */; };
|
||||
D2FA83D62515021F00564112 /* NotificationStatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D52515021F00564112 /* NotificationStatusBar.swift */; };
|
||||
D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB151A23A2B65B00C20E10 /* MoleculeContainer.swift */; };
|
||||
D2FB151D23A40F1500C20E10 /* MoleculeStackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB151C23A40F1500C20E10 /* MoleculeStackItem.swift */; };
|
||||
DB06250B2293456500B72DD3 /* LeftRightLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */; };
|
||||
@ -806,6 +810,7 @@
|
||||
D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccordionMoleculeTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D22D8392241C27B100D3DF69 /* TemplateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModel.swift; sourceTree = "<group>"; };
|
||||
D22D8394241FB41200D3DF69 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = "<group>"; };
|
||||
D23118B225124E18001C8440 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
|
||||
D2351C7924A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinksModel.swift; sourceTree = "<group>"; };
|
||||
D2351C7B24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinks.swift; sourceTree = "<group>"; };
|
||||
D236E5B2241FEB1000C38625 /* ListTwoColumnPriceDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListTwoColumnPriceDescription.swift; sourceTree = "<group>"; };
|
||||
@ -970,6 +975,9 @@
|
||||
D2E2A99E23E07F8A000B42E6 /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = "<group>"; };
|
||||
D2E2A9A023E095AB000B42E6 /* ButtonModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonModelProtocol.swift; sourceTree = "<group>"; };
|
||||
D2E2A9A223E096B1000B42E6 /* DisableableModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableableModelProtocol.swift; sourceTree = "<group>"; };
|
||||
D2FA83D12513EA6900564112 /* NotificationXButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationXButton.swift; sourceTree = "<group>"; };
|
||||
D2FA83D32514F80C00564112 /* CollapsableNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotification.swift; sourceTree = "<group>"; };
|
||||
D2FA83D52515021F00564112 /* NotificationStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusBar.swift; sourceTree = "<group>"; };
|
||||
D2FB151A23A2B65B00C20E10 /* MoleculeContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeContainer.swift; sourceTree = "<group>"; };
|
||||
D2FB151C23A40F1500C20E10 /* MoleculeStackItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeStackItem.swift; sourceTree = "<group>"; };
|
||||
DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeftRightLabelView.swift; sourceTree = "<group>"; };
|
||||
@ -2077,9 +2085,13 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */,
|
||||
D2FA83D12513EA6900564112 /* NotificationXButton.swift */,
|
||||
D2CAC7CC251104FE00C75681 /* NotificationModel.swift */,
|
||||
D23118B225124E18001C8440 /* Notification.swift */,
|
||||
D2CAC7D02511058C00C75681 /* MVMCoreUITopAlertMainView+Extension.swift */,
|
||||
D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */,
|
||||
D2FA83D32514F80C00564112 /* CollapsableNotification.swift */,
|
||||
D2FA83D52515021F00564112 /* NotificationStatusBar.swift */,
|
||||
D2CAC7D2251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift */,
|
||||
);
|
||||
path = TopNotification;
|
||||
@ -2460,6 +2472,7 @@
|
||||
C6FA7D5323C77A4A00A3614A /* StringAndMoleculeStack.swift in Sources */,
|
||||
32F8804624765C6E00C2ACB3 /* ListLeftVariableNumberedListAllTextAndLinksModel.swift in Sources */,
|
||||
011D958524042432000E3791 /* RulesProtocol.swift in Sources */,
|
||||
D23118B325124E18001C8440 /* Notification.swift in Sources */,
|
||||
AA9972502475309F00FC7472 /* ListLeftVariableIconAllTextLinksModel.swift in Sources */,
|
||||
AA69AAF62445BF5700AF3D3B /* ListLeftVariableCheckboxBodyText.swift in Sources */,
|
||||
D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */,
|
||||
@ -2515,6 +2528,7 @@
|
||||
D2A92882241AAB67004E01C6 /* ScrollingViewController.swift in Sources */,
|
||||
C695A67F23C9830600BFB94E /* UnOrderedListModel.swift in Sources */,
|
||||
0AE98BB523FF18D2004C5109 /* Arrow.swift in Sources */,
|
||||
D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */,
|
||||
D2D90B442404789000DD6EC9 /* MoleculeContainerProtocol.swift in Sources */,
|
||||
0A7ECC5F243CEB1200C828E8 /* ColorViewWithLabel.swift in Sources */,
|
||||
94C0150A24215643005811A9 /* ActionTopAlertModel.swift in Sources */,
|
||||
@ -2560,6 +2574,7 @@
|
||||
AAB8549824DC01BD00477C40 /* ListThreeColumnBillHistoryDividerModel.swift in Sources */,
|
||||
D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */,
|
||||
D2B1E3E522F37D6A0065F95C /* ImageHeadlineBody.swift in Sources */,
|
||||
D2FA83D62515021F00564112 /* NotificationStatusBar.swift in Sources */,
|
||||
0A21DB94235E24ED00C160A2 /* DigitEntryField.swift in Sources */,
|
||||
AA56A211243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift in Sources */,
|
||||
D264FA8C243BCD8E00D98315 /* CollectionTemplateModel.swift in Sources */,
|
||||
@ -2605,6 +2620,7 @@
|
||||
C7192E7D23C301750050C2A0 /* HeadLineBodyCaretLinkImage.swift in Sources */,
|
||||
D29DF13221E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m in Sources */,
|
||||
BB1D17E2244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift in Sources */,
|
||||
D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */,
|
||||
0A7EF86323D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift in Sources */,
|
||||
0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */,
|
||||
D236E5B5241FEB1000C38625 /* ListTwoColumnPriceDescriptionModel.swift in Sources */,
|
||||
|
||||
@ -233,8 +233,8 @@ import Foundation
|
||||
MoleculeObjectMapping.shared()?.register(viewClass: LockupsPlanSMLXL.self, viewModelClass: LockupsPlanSMLXLModel.self)
|
||||
|
||||
// MARK: - Top Notifications
|
||||
MoleculeObjectMapping.shared()?.register(viewClass: MVMCoreUITopAlertMainView.self, viewModelClass: NotificationModel.self)
|
||||
MoleculeObjectMapping.shared()?.register(viewClass: MVMCoreUITopAlertExpandableView.self, viewModelClass: CollapsableNotificationModel.self)
|
||||
MoleculeObjectMapping.shared()?.register(viewClass: NotificationView.self, viewModelClass: NotificationModel.self)
|
||||
MoleculeObjectMapping.shared()?.register(viewClass: CollapsableNotification.self, viewModelClass: CollapsableNotificationModel.self)
|
||||
|
||||
// MARK:- Helper models
|
||||
try? ModelRegistry.register(RuleRequiredModel.self)
|
||||
|
||||
@ -0,0 +1,168 @@
|
||||
//
|
||||
// CollapsableNotification.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Scott Pfeil on 9/18/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objcMembers open class CollapsableNotification: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Outlets
|
||||
//--------------------------------------------------
|
||||
|
||||
public let topView = NotificationStatusBar()
|
||||
public let bottomView = NotificationView()
|
||||
public var verticalStack: UIStackView!
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Life Cycle
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
verticalStack = UIStackView(arrangedSubviews: [topView, bottomView])
|
||||
verticalStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
verticalStack.axis = .vertical
|
||||
verticalStack.alignment = .fill
|
||||
verticalStack.distribution = .fill
|
||||
addSubview(verticalStack)
|
||||
NSLayoutConstraint.constraintPinSubview(verticalStack, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: 0, pinLeft: true, leftConstant: 0, pinRight: true, rightConstant: 0)
|
||||
|
||||
reset()
|
||||
}
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
verticalStack.updateView(size)
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
verticalStack.reset()
|
||||
backgroundColor = .mvmGreen()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Molecule
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
guard let model = model as? CollapsableNotificationModel else { return }
|
||||
topView.label.set(with: model.topLabel, delegateObject, additionalData)
|
||||
topView.button.set(with: model.topAction, delegateObject: delegateObject, additionalData: additionalData)
|
||||
bottomView.set(with: model, delegateObject, additionalData)
|
||||
updateAccessibilityLabel()
|
||||
|
||||
topView.isHidden = !model.alwaysShowTopLabel && !model.initiallyCollapsed
|
||||
topView.button.isUserInteractionEnabled = model.initiallyCollapsed
|
||||
bottomView.isHidden = model.initiallyCollapsed
|
||||
verticalStack.layoutIfNeeded()
|
||||
|
||||
if !model.initiallyCollapsed {
|
||||
collapse(with: .now() + DispatchTimeInterval.seconds(model.collapseTime))
|
||||
}
|
||||
}
|
||||
|
||||
open func collapse(with delay: DispatchTime) {
|
||||
DispatchQueue.main.asyncAfter(deadline: delay) { [weak self] in
|
||||
self?.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
open func collapse(animated: Bool = true) {
|
||||
let animation = { [weak self] in
|
||||
self?.topView.isHidden = false
|
||||
self?.bottomView.isHidden = true
|
||||
self?.verticalStack.layoutIfNeeded()
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.5, animations: animation) { [weak self] (finished) in
|
||||
self?.topView.button.isUserInteractionEnabled = true
|
||||
}
|
||||
} else {
|
||||
animation()
|
||||
}
|
||||
}
|
||||
|
||||
open func expand(topViewShowing: Bool = false, animated: Bool = true) {
|
||||
topView.button.isUserInteractionEnabled = false
|
||||
let animation = { [weak self] in
|
||||
self?.topView.isHidden = !topViewShowing
|
||||
self?.bottomView.isHidden = false
|
||||
self?.verticalStack.layoutIfNeeded()
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.5, animations: animation) { [weak self] (finished) in
|
||||
|
||||
}
|
||||
} else {
|
||||
animation()
|
||||
}
|
||||
}
|
||||
|
||||
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
return 96
|
||||
}
|
||||
/*
|
||||
func getAccessibilityMessage() -> String? {
|
||||
|
||||
guard let leftImageLabel = leftImage.imageView.accessibilityLabel else {
|
||||
return eyebrowHeadlineBodyLink.getAccessibilityMessage()
|
||||
}
|
||||
|
||||
guard let label = eyebrowHeadlineBodyLink.getAccessibilityMessage() else {
|
||||
return leftImageLabel
|
||||
}
|
||||
|
||||
return leftImageLabel + ", " + label
|
||||
}*/
|
||||
|
||||
func updateAccessibilityLabel() {
|
||||
/*headline.accessibilityLabel = headline.text
|
||||
MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: headline)
|
||||
|
||||
body.accessibilityLabel = body.text
|
||||
MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: body)
|
||||
|
||||
|
||||
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
|
||||
isAccessibilityElement = !linkShowing
|
||||
accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none
|
||||
|
||||
if !linkShowing {
|
||||
// Make whole cell focusable if no link.
|
||||
accessibilityLabel = getAccessibilityMessage()
|
||||
} else if let accessoryView = accessoryView {
|
||||
// Both caret and link. Read all content on caret.
|
||||
accessoryView.accessibilityLabel = getAccessibilityMessage()
|
||||
accessibilityElements = [accessoryView, eyebrowHeadlineBodyLink.link]
|
||||
} else {
|
||||
// Only link. Manually add accessibility elements to ensure they are read in the right order.
|
||||
var elements: [Any] = []
|
||||
|
||||
if let leftImageLabel = leftImage.imageView.accessibilityLabel, !leftImageLabel.isEmpty {
|
||||
elements.append(leftImage.imageView)
|
||||
}
|
||||
|
||||
if let otherElements = eyebrowHeadlineBodyLink.getAccessibilityElements() {
|
||||
elements.append(otherElements)
|
||||
}
|
||||
|
||||
accessibilityElements = elements
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
extension CollapsableNotification: StatusBarUI {
|
||||
func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) {
|
||||
let color = backgroundColor ?? UIColor.mvmGreen
|
||||
var greyScale: CGFloat = 0
|
||||
topView.label.textColor.getWhite(&greyScale, alpha: nil)
|
||||
return (color, greyScale > 0.5 ? .lightContent : .default)
|
||||
}
|
||||
}
|
||||
@ -8,16 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public class CollapsableNotificationModel: MoleculeModelProtocol {
|
||||
public static var identifier: String = "collapsableNotification"
|
||||
public var moleculeName: String = CollapsableNotificationModel.identifier
|
||||
public var backgroundColor: Color?
|
||||
public class CollapsableNotificationModel: NotificationModel {
|
||||
public class override var identifier: String {
|
||||
return "collapsableNotification"
|
||||
}
|
||||
public var topLabel: LabelModel
|
||||
public var topAction: ActionModelProtocol?
|
||||
public var headline: LabelModel
|
||||
public var body: LabelModel?
|
||||
public var button: ButtonModel?
|
||||
public var closeButton: NotificationXButtonModel?
|
||||
public var alwaysShowTopLabel = false
|
||||
public var collapseTime: Int = 5
|
||||
public var initiallyCollapsed = false
|
||||
@ -25,18 +21,23 @@ public class CollapsableNotificationModel: MoleculeModelProtocol {
|
||||
|
||||
init(with topLabel: LabelModel, headline: LabelModel) {
|
||||
self.topLabel = topLabel
|
||||
self.headline = headline
|
||||
super.init(with: headline)
|
||||
}
|
||||
|
||||
override func setDefault() {
|
||||
super.setDefault()
|
||||
if topLabel.textColor == nil {
|
||||
topLabel.textColor = Color(uiColor: .white)
|
||||
}
|
||||
if topLabel.textAlignment == nil {
|
||||
topLabel.textAlignment = .center
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case topLabel
|
||||
case topAction
|
||||
case headline
|
||||
case body
|
||||
case button
|
||||
case closeButton
|
||||
case alwaysShowTopLabel
|
||||
case collapseTime
|
||||
case initiallyCollapsed
|
||||
@ -46,12 +47,7 @@ public class CollapsableNotificationModel: MoleculeModelProtocol {
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
topLabel = try typeContainer.decode(LabelModel.self, forKey: .topLabel)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
topAction = try typeContainer.decodeModelIfPresent(codingKey: .topAction)
|
||||
headline = try typeContainer.decode(LabelModel.self, forKey: .headline)
|
||||
body = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .body)
|
||||
button = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .button)
|
||||
closeButton = try typeContainer.decodeIfPresent(NotificationXButtonModel.self, forKey: .closeButton)
|
||||
if let alwaysShowTopLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysShowTopLabel) {
|
||||
self.alwaysShowTopLabel = alwaysShowTopLabel
|
||||
}
|
||||
@ -62,18 +58,15 @@ public class CollapsableNotificationModel: MoleculeModelProtocol {
|
||||
self.initiallyCollapsed = initiallyCollapsed
|
||||
}
|
||||
pages = try typeContainer.decodeIfPresent([String].self, forKey: .pages)
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encode(topLabel, forKey: .topLabel)
|
||||
try container.encodeModelIfPresent(topAction, forKey: .topAction)
|
||||
try container.encode(headline, forKey: .headline)
|
||||
try container.encodeIfPresent(body, forKey: .body)
|
||||
try container.encodeIfPresent(button, forKey: .button)
|
||||
try container.encodeIfPresent(closeButton, forKey: .closeButton)
|
||||
try container.encode(alwaysShowTopLabel, forKey: .alwaysShowTopLabel)
|
||||
try container.encode(collapseTime, forKey: .collapseTime)
|
||||
try container.encode(initiallyCollapsed, forKey: .initiallyCollapsed)
|
||||
|
||||
117
MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift
Normal file
117
MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift
Normal file
@ -0,0 +1,117 @@
|
||||
//
|
||||
// Notification.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Scott Pfeil on 9/16/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objcMembers open class NotificationView: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Outlets
|
||||
//--------------------------------------------------
|
||||
|
||||
public let headline = Label(fontStyle: .BoldBodySmall)
|
||||
public let body = Label(fontStyle: .RegularBodySmall)
|
||||
public let button = PillButton(asPrimaryButton: false, makeTiny: true)
|
||||
public let closeButton = NotificationXButton()
|
||||
public var labelStack: Stack<StackModel>!
|
||||
public var horizontalStack: Stack<StackModel>!
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Life Cycle
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func setupView() {
|
||||
super.setupView()
|
||||
reset()
|
||||
|
||||
labelStack = Stack<StackModel>.createStack(with: [headline, body], spacing: 0)
|
||||
horizontalStack = Stack<StackModel>.createStack(with: [(view: labelStack, model: StackItemModel()),(view: button, model: StackItemModel(horizontalAlignment: .fill)),(view: closeButton, model: StackItemModel(horizontalAlignment: .fill))], axis: .horizontal)
|
||||
addSubview(horizontalStack)
|
||||
NSLayoutConstraint.constraintPinSubview(horizontalStack, pinTop: true, topConstant: PaddingTwo, pinBottom: true, bottomConstant: PaddingTwo, pinLeft: true, leftConstant: PaddingThree, pinRight: true, rightConstant: PaddingThree)
|
||||
labelStack.restack()
|
||||
horizontalStack.restack()
|
||||
|
||||
heightAnchor.constraint(equalToConstant: 96).isActive = true
|
||||
}
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
horizontalStack.updateView(size)
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
backgroundColor = .mvmGreen()
|
||||
headline.textColor = .white
|
||||
body.textColor = .white
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Molecule
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
guard let model = model as? NotificationModel else { return }
|
||||
labelStack.updateContainedMolecules(with: [model.headline, model.body], delegateObject, nil)
|
||||
horizontalStack.updateContainedMolecules(with: [labelStack.stackModel, model.button, model.closeButton], delegateObject, nil)
|
||||
|
||||
updateAccessibilityLabel()
|
||||
}
|
||||
|
||||
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
return 96
|
||||
}
|
||||
/*
|
||||
func getAccessibilityMessage() -> String? {
|
||||
|
||||
guard let leftImageLabel = leftImage.imageView.accessibilityLabel else {
|
||||
return eyebrowHeadlineBodyLink.getAccessibilityMessage()
|
||||
}
|
||||
|
||||
guard let label = eyebrowHeadlineBodyLink.getAccessibilityMessage() else {
|
||||
return leftImageLabel
|
||||
}
|
||||
|
||||
return leftImageLabel + ", " + label
|
||||
}*/
|
||||
|
||||
func updateAccessibilityLabel() {
|
||||
/*headline.accessibilityLabel = headline.text
|
||||
MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: headline)
|
||||
|
||||
body.accessibilityLabel = body.text
|
||||
MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: body)
|
||||
|
||||
|
||||
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
|
||||
isAccessibilityElement = !linkShowing
|
||||
accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none
|
||||
|
||||
if !linkShowing {
|
||||
// Make whole cell focusable if no link.
|
||||
accessibilityLabel = getAccessibilityMessage()
|
||||
} else if let accessoryView = accessoryView {
|
||||
// Both caret and link. Read all content on caret.
|
||||
accessoryView.accessibilityLabel = getAccessibilityMessage()
|
||||
accessibilityElements = [accessoryView, eyebrowHeadlineBodyLink.link]
|
||||
} else {
|
||||
// Only link. Manually add accessibility elements to ensure they are read in the right order.
|
||||
var elements: [Any] = []
|
||||
|
||||
if let leftImageLabel = leftImage.imageView.accessibilityLabel, !leftImageLabel.isEmpty {
|
||||
elements.append(leftImage.imageView)
|
||||
}
|
||||
|
||||
if let otherElements = eyebrowHeadlineBodyLink.getAccessibilityElements() {
|
||||
elements.append(otherElements)
|
||||
}
|
||||
|
||||
accessibilityElements = elements
|
||||
}*/
|
||||
}
|
||||
}
|
||||
@ -9,8 +9,9 @@
|
||||
import Foundation
|
||||
|
||||
public class NotificationModel: MoleculeModelProtocol {
|
||||
public static var identifier: String = "notification"
|
||||
public var moleculeName: String = NotificationModel.identifier
|
||||
public class var identifier: String {
|
||||
return "notification"
|
||||
}
|
||||
public var backgroundColor: Color?
|
||||
public var headline: LabelModel
|
||||
public var body: LabelModel?
|
||||
@ -20,4 +21,51 @@ public class NotificationModel: MoleculeModelProtocol {
|
||||
init(with headline: LabelModel) {
|
||||
self.headline = headline
|
||||
}
|
||||
|
||||
func setDefault() {
|
||||
if backgroundColor == nil {
|
||||
backgroundColor = Color(uiColor: .mvmGreen())
|
||||
}
|
||||
if headline.textColor == nil {
|
||||
headline.textColor = Color(uiColor: .white)
|
||||
}
|
||||
if body?.textColor == nil {
|
||||
body?.textColor = Color(uiColor: .white)
|
||||
}
|
||||
if button?.style == nil {
|
||||
button?.style = .secondary
|
||||
}
|
||||
button?.size = .tiny
|
||||
button?.enabledTextColor = Color(uiColor: .white)
|
||||
button?.enabledBorderColor = Color(uiColor: .white)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case headline
|
||||
case body
|
||||
case button
|
||||
case closeButton
|
||||
}
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
headline = try typeContainer.decode(LabelModel.self, forKey: .headline)
|
||||
body = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .body)
|
||||
button = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .button)
|
||||
closeButton = try typeContainer.decodeIfPresent(NotificationXButtonModel.self, forKey: .closeButton)
|
||||
setDefault()
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encode(headline, forKey: .headline)
|
||||
try container.encodeIfPresent(body, forKey: .body)
|
||||
try container.encodeIfPresent(button, forKey: .button)
|
||||
try container.encodeIfPresent(closeButton, forKey: .closeButton)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
//
|
||||
// NotificationStatusBar.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Scott Pfeil on 9/18/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objcMembers open class NotificationStatusBar: View {
|
||||
public let label: Label = {
|
||||
let label = Label(fontStyle: .BoldBodySmall)
|
||||
label.numberOfLines = 1
|
||||
label.textAlignment = .center
|
||||
label.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
return label
|
||||
}()
|
||||
|
||||
public let button: Button = {
|
||||
let button = Button(type: .custom)
|
||||
button.backgroundColor = .clear
|
||||
button.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
button.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
return button
|
||||
}()
|
||||
|
||||
public override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
addSubview(label)
|
||||
NSLayoutConstraint.constraintPinSubview(label, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: 0, pinLeft: true, leftConstant: PaddingThree, pinRight: true, rightConstant: PaddingThree)
|
||||
|
||||
addSubview(button)
|
||||
NSLayoutConstraint.constraintPinSubview(button, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: -5, pinLeft: true, leftConstant: 0, pinRight: true, rightConstant: 0)
|
||||
|
||||
// Listen for status bar touches.
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(pressed(_:)), name: NSNotification.Name(rawValue: NotificationStatusBarTouched), object: nil)
|
||||
}
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
label.updateView(size)
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
label.setFontStyle(.BoldBodySmall)
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
}
|
||||
|
||||
@objc func pressed(_ sender: Notification) {
|
||||
button.callActionBlock(button)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
//
|
||||
// NotificationXButton.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Scott Pfeil on 9/17/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objcMembers open class NotificationXButton: Button {
|
||||
|
||||
open func closeTopAlert() {
|
||||
if let delegate = MVMCoreUITopAlertView.sharedGlobal()?.animationDelegate {
|
||||
delegate.topAlertCloseButtonPressed()
|
||||
} else {
|
||||
MVMCoreUISession.sharedGlobal()?.topAlertView?.hideAlertView(true, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
open override func setupView() {
|
||||
if let image = MVMCoreUIUtility.imageNamed("nav_close")?.withRenderingMode(.alwaysTemplate) {
|
||||
setImage(image, for: .normal)
|
||||
}
|
||||
tintColor = .white
|
||||
adjustsImageWhenHighlighted = false
|
||||
accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "AccCloseButton")
|
||||
}
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
guard let model = model as? NotificationXButtonModel else { return }
|
||||
tintColor = model.color.uiColor
|
||||
|
||||
// temporary
|
||||
if model.action.actionType == ActionNoopModel.identifier {
|
||||
addActionBlock(event: .touchUpInside) { (button) in
|
||||
(button as? NotificationXButton)?.closeTopAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,27 +8,32 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public class NotificationXButtonModel: Codable {
|
||||
public var color: Color?
|
||||
public var action: ActionModelProtocol?
|
||||
public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtocol {
|
||||
public static var identifier: String = "notificationXButton"
|
||||
public var backgroundColor: Color?
|
||||
public var color = Color(uiColor: .white)
|
||||
public var action: ActionModelProtocol = ActionNoopModel()
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case moleculeName
|
||||
case color
|
||||
case action
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
public init() {}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
color = try typeContainer.decodeIfPresent(Color.self, forKey: .color)
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) ?? Color(uiColor: .white)
|
||||
if let action: ActionModelProtocol = try typeContainer.decodeModelIfPresent(codingKey: .action) {
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(color, forKey: .color)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(color, forKey: .color)
|
||||
try container.encodeModel(action, forKey: .action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,12 +72,12 @@ public typealias ButtonAction = (Button) -> ()
|
||||
buttonAction?(self)
|
||||
}
|
||||
|
||||
open func set(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
|
||||
open func set(with actionModel: ActionModelProtocol?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
|
||||
self.actionModel = actionModel
|
||||
buttonDelegate = delegateObject?.buttonDelegate
|
||||
|
||||
addActionBlock(event: .touchUpInside) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
guard let self = self, let actionModel = actionModel else { return }
|
||||
Self.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,21 +56,23 @@ public extension MVMCoreUITopAlertView {
|
||||
}
|
||||
|
||||
/// Returns the top alert molecule to use and status bar color legacy style.
|
||||
@objc func molecule(for topAlertObject: MVMCoreTopAlertObject, statusBarColor: AutoreleasingUnsafeMutablePointer<UIColor?>?, statusBarStyle: UnsafeMutablePointer<UIStatusBarStyle>?) -> MVMCoreUITopAlertBaseView? {
|
||||
@objc func molecule(for topAlertObject: MVMCoreTopAlertObject, statusBarColor: AutoreleasingUnsafeMutablePointer<UIColor?>?, statusBarStyle: UnsafeMutablePointer<UIStatusBarStyle>?) -> UIView? {
|
||||
do {
|
||||
let delegateObject = MVMCoreUIDelegateObject.create(withDelegateForAll: self)
|
||||
guard let json = topAlertObject.json else { return nil }
|
||||
let model = try TopNotificationModel.decode(json: json, delegateObject: delegateObject)
|
||||
guard let molecule = MoleculeObjectMapping.shared()?.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil),
|
||||
let view = molecule as? MVMCoreUITopAlertBaseView else {
|
||||
guard let molecule = MoleculeObjectMapping.shared()?.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil)/*,
|
||||
let view = molecule as? MVMCoreUITopAlertBaseView*/ else {
|
||||
throw ModelRegistry.Error.decoderOther(message: "Molecule not a top alert")
|
||||
}
|
||||
if let castView = view as? StatusBarUI {
|
||||
if let castView = molecule as? StatusBarUI {
|
||||
let (color, style) = castView.getStatusBarUI()
|
||||
statusBarColor?.pointee = color
|
||||
statusBarStyle?.pointee = style
|
||||
}
|
||||
return view
|
||||
// Temporary
|
||||
molecule.heightAnchor.constraint(lessThanOrEqualToConstant: 150).isActive = true
|
||||
return molecule
|
||||
} catch {
|
||||
if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "\(self)") {
|
||||
MVMCoreUILoggingHandler.shared()?.addError(toLog: errorObject)
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
#import <MVMCoreUI/ButtonDelegateProtocol.h>
|
||||
|
||||
@class MVMCoreTopAlertObject;
|
||||
@class MVMCoreUITopAlertBaseView;
|
||||
|
||||
@interface MVMCoreUITopAlertView : UIView <MVMCoreViewProtocol, MVMCoreTopAlertViewProtocol, MVMCoreLoadDelegateProtocol, MVMCoreActionDelegateProtocol, MVMCorePresentationDelegateProtocol, ButtonDelegateProtocol>
|
||||
|
||||
@ -45,7 +44,7 @@
|
||||
- (void)resetDefaultBackgroundColor:(nullable UIColor *)backgroundColor basedOnStatusBarStyle:(UIStatusBarStyle)style;
|
||||
|
||||
// Can be subclassed for custom views.
|
||||
- (nonnull MVMCoreUITopAlertBaseView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id <MVMCoreTopAlertAnimationDelegateProtocol>)animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle;
|
||||
- (nonnull UIView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id <MVMCoreTopAlertAnimationDelegateProtocol>)animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle;
|
||||
|
||||
/// Get the background color based on the type
|
||||
- (nonnull UIColor *)getBackgroundColorForType:(nullable NSString *)type;
|
||||
|
||||
@ -124,7 +124,7 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed.";
|
||||
}
|
||||
}
|
||||
|
||||
- (nonnull MVMCoreUITopAlertBaseView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id <MVMCoreTopAlertAnimationDelegateProtocol>)animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle {
|
||||
- (nonnull UIView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id <MVMCoreTopAlertAnimationDelegateProtocol>)animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle {
|
||||
if (topAlertObject.json) {
|
||||
return [self moleculeFor:topAlertObject statusBarColor:statusBarColor statusBarStyle:statusBarStyle];
|
||||
} else {
|
||||
@ -169,8 +169,10 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed.";
|
||||
|
||||
UIColor *statusBarColor = nil;
|
||||
UIStatusBarStyle statusBarStyle = UIStatusBarStyleDefault;
|
||||
MVMCoreUITopAlertBaseView *view = [self topAlertViewForTopAlertObject:topAlertObject animationDelegate:animationDelegate statusBarColor:&statusBarColor statusBarStyle:&statusBarStyle];
|
||||
[view updateView:CGRectGetWidth(self.bounds)];
|
||||
UIView *view = [self topAlertViewForTopAlertObject:topAlertObject animationDelegate:animationDelegate statusBarColor:&statusBarColor statusBarStyle:&statusBarStyle];
|
||||
if ([view conformsToProtocol:@protocol(MVMCoreViewProtocol)]) {
|
||||
[((UIView <MVMCoreViewProtocol>*)view) updateView:CGRectGetWidth(self.bounds)];
|
||||
}
|
||||
if (!statusBarColor) {
|
||||
statusBarColor = [UIColor whiteColor];
|
||||
}
|
||||
@ -185,7 +187,7 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed.";
|
||||
[[MVMCoreUISession sharedGlobal].splitViewController.parentViewController setNeedsStatusBarAppearanceUpdate];
|
||||
}
|
||||
|
||||
- (void)showAlertView:(nullable MVMCoreUITopAlertBaseView *)view topAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject completionHandler:(void (^ __nullable)(BOOL finished))completionHandler {
|
||||
- (void)showAlertView:(nullable UIView *)view topAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject completionHandler:(void (^ __nullable)(BOOL finished))completionHandler {
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) {
|
||||
@ -205,8 +207,10 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed.";
|
||||
} completion:^(BOOL finished) {
|
||||
[weakSelf.superview layoutIfNeeded];
|
||||
[weakSelf.animationDelegate topAlertViewFinishAnimation];
|
||||
[view handleAccessibility];
|
||||
|
||||
if ([view isKindOfClass:[MVMCoreUITopAlertBaseView class]]) {
|
||||
[((MVMCoreUITopAlertBaseView *)view) handleAccessibility];
|
||||
}
|
||||
|
||||
[MVMCoreDispatchUtility performBlockInBackground:^{
|
||||
if ([weakSelf.topAlertObject.delegate respondsToSelector:@selector(topAlertViewShown:topAlertObject:)]) {
|
||||
[weakSelf.topAlertObject.delegate topAlertViewShown:view topAlertObject:topAlertObject];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user