814 lines
30 KiB
Swift
814 lines
30 KiB
Swift
//
|
|
// Copyright 2026 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
import SwiftUI
|
|
|
|
class MemberLabelViewController: OWSViewController, UITextFieldDelegate {
|
|
private let initialEmoji: String?
|
|
private let initialMemberLabel: String?
|
|
private var updatedMemberLabel: String?
|
|
private var updatedEmoji: String?
|
|
private var addEmojiButton = UIButton(type: .system)
|
|
private var previewContainer: UIStackView?
|
|
private let stackView = UIStackView()
|
|
private let textField = UITextField()
|
|
private var characterCountLabel = UILabel()
|
|
private var clearButton = UIButton(type: .system)
|
|
|
|
// Views that may be updated when thread info changes
|
|
private var contactListStackView: UIStackView?
|
|
private var noOtherMembersLabelContainer: UIView?
|
|
|
|
private var groupNameColors: GroupNameColors
|
|
private var groupMemberLabelsWithoutLocalUser: [SignalServiceAddress: MemberLabelForRendering]
|
|
private var groupModel: TSGroupModelV2
|
|
private let db: DB
|
|
private let contactManager: OWSContactsManager
|
|
private let localIdentifiers: LocalIdentifiers
|
|
|
|
weak var updateDelegate: MemberLabelCoordinator?
|
|
|
|
private static let maxCharCount = 24
|
|
private static let showCharacterCountMax = 9
|
|
|
|
private var onDismiss: () -> Void
|
|
|
|
init(
|
|
memberLabel: String? = nil,
|
|
emoji: String? = nil,
|
|
groupNameColors: GroupNameColors,
|
|
groupMemberLabelsWithoutLocalUser: [SignalServiceAddress: MemberLabelForRendering],
|
|
groupModel: TSGroupModelV2,
|
|
db: DB,
|
|
contactManager: OWSContactsManager,
|
|
localIdentifiers: LocalIdentifiers,
|
|
onDismiss: @escaping () -> Void,
|
|
) {
|
|
self.initialMemberLabel = memberLabel
|
|
self.initialEmoji = emoji
|
|
self.updatedMemberLabel = memberLabel
|
|
self.updatedEmoji = emoji
|
|
self.groupNameColors = groupNameColors
|
|
self.groupModel = groupModel
|
|
textField.text = memberLabel
|
|
self.groupMemberLabelsWithoutLocalUser = groupMemberLabelsWithoutLocalUser
|
|
self.db = db
|
|
self.contactManager = contactManager
|
|
self.localIdentifiers = localIdentifiers
|
|
self.onDismiss = onDismiss
|
|
|
|
super.init()
|
|
|
|
view.backgroundColor = UIColor.Signal.groupedBackground
|
|
addNavigationTitleView(groupName: groupModel.groupNameOrDefault)
|
|
navigationItem.rightBarButtonItem = .init(barButtonSystemItem: .done, target: self, action: #selector(didTapDone))
|
|
|
|
navigationItem.leftBarButtonItem = .cancelButton(
|
|
dismissingFrom: self,
|
|
hasUnsavedChanges: { [weak self] in
|
|
guard let self else { return false }
|
|
return updatedMemberLabel != initialMemberLabel || updatedEmoji != initialEmoji
|
|
},
|
|
completion: {
|
|
onDismiss()
|
|
},
|
|
)
|
|
|
|
navigationItem.rightBarButtonItem?.tintColor = UIColor.Signal.ultramarine
|
|
navigationItem.rightBarButtonItem?.isEnabled = false
|
|
}
|
|
|
|
func updateWithNewThreadInfo(
|
|
groupNameColors: GroupNameColors,
|
|
groupMemberLabelsWithoutLocalUser: [SignalServiceAddress: MemberLabelForRendering],
|
|
groupModel: TSGroupModelV2,
|
|
) {
|
|
self.groupNameColors = groupNameColors
|
|
self.groupModel = groupModel
|
|
self.groupMemberLabelsWithoutLocalUser = groupMemberLabelsWithoutLocalUser
|
|
|
|
addNavigationTitleView(groupName: groupModel.groupNameOrDefault)
|
|
reloadMessagePreview()
|
|
|
|
// The group member labels section is the only thing that needs updating if the thread is reloaded.
|
|
noOtherMembersLabelContainer?.removeFromSuperview()
|
|
contactListStackView?.removeFromSuperview()
|
|
contactListStackView = nil
|
|
noOtherMembersLabelContainer = nil
|
|
buildGroupMembershipSection()
|
|
}
|
|
|
|
func addNavigationTitleView(groupName: String) {
|
|
let titleLabel = UILabel()
|
|
titleLabel.text = OWSLocalizedString(
|
|
"MEMBER_LABEL_VIEW_TITLE",
|
|
comment: "Title for a view where users can edit and preview their member label.",
|
|
)
|
|
titleLabel.font = .dynamicTypeSubheadline.semibold()
|
|
titleLabel.textColor = UIColor.Signal.label
|
|
titleLabel.textAlignment = .center
|
|
|
|
let subtitleLabel = UILabel()
|
|
subtitleLabel.text = groupName
|
|
subtitleLabel.font = .dynamicTypeCaption1.semibold()
|
|
subtitleLabel.textColor = UIColor.Signal.secondaryLabel
|
|
subtitleLabel.textAlignment = .center
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
|
stackView.axis = .vertical
|
|
stackView.alignment = .center
|
|
stackView.spacing = 0
|
|
|
|
navigationItem.titleView = stackView
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
let scrollView = UIScrollView()
|
|
view.addSubview(scrollView)
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
])
|
|
|
|
createInitialViews()
|
|
|
|
scrollView.addSubview(stackView)
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
|
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
|
|
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
|
|
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
|
stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
|
|
])
|
|
scrollView.keyboardDismissMode = .onDrag
|
|
|
|
// Group Section label
|
|
let sectionLabel = UILabel()
|
|
sectionLabel.text = OWSLocalizedString(
|
|
"MEMBER_LABEL_GROUP_LABELS_SECTION_TITLE",
|
|
comment: "Section header for a list of group member labels",
|
|
)
|
|
sectionLabel.font = .dynamicTypeBodyClamped.semibold()
|
|
stackView.addArrangedSubview(sectionLabel)
|
|
stackView.setCustomSpacing(8, after: sectionLabel)
|
|
|
|
buildGroupMembershipSection()
|
|
|
|
textField.becomeFirstResponder()
|
|
|
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
|
view.addGestureRecognizer(tapGesture)
|
|
}
|
|
|
|
private func createInitialViews() {
|
|
stackView.axis = .vertical
|
|
stackView.spacing = 20
|
|
|
|
let subtitleLabel = UILabel()
|
|
subtitleLabel.text = OWSLocalizedString(
|
|
"MEMBER_LABEL_VIEW_SUBTITLE",
|
|
comment: "Subtitle for a view where users can edit and preview their member label.",
|
|
)
|
|
subtitleLabel.numberOfLines = 0
|
|
subtitleLabel.font = .dynamicTypeCaption1Clamped
|
|
subtitleLabel.textColor = UIColor.Signal.secondaryLabel
|
|
subtitleLabel.textAlignment = .center
|
|
|
|
stackView.addArrangedSubview(subtitleLabel)
|
|
|
|
let textFieldStack = UIStackView()
|
|
textFieldStack.layer.cornerRadius = 27
|
|
textFieldStack.backgroundColor = UIColor.Signal.tertiaryBackground
|
|
textFieldStack.axis = .horizontal
|
|
textFieldStack.alignment = .center
|
|
textFieldStack.distribution = .fill
|
|
textFieldStack.spacing = 8
|
|
textFieldStack.isLayoutMarginsRelativeArrangement = true
|
|
textFieldStack.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
|
|
|
|
if let initialEmoji {
|
|
addEmojiButton.setImage(nil, for: .normal)
|
|
addEmojiButton.setTitle(initialEmoji, for: .normal)
|
|
addEmojiButton.titleLabel?.font = .dynamicTypeTitle3Clamped
|
|
} else {
|
|
addEmojiButton.setImage(UIImage(named: "emoji-plus"), for: .normal)
|
|
addEmojiButton.tintColor = UIColor.Signal.secondaryLabel
|
|
}
|
|
addEmojiButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
addEmojiButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
addEmojiButton.addTarget(self, action: #selector(didTapEmojiPicker), for: .touchUpInside)
|
|
|
|
textField.placeholder = OWSLocalizedString(
|
|
"MEMBER_LABEL_VIEW_PLACEHOLDER_TEXT",
|
|
comment: "Placeholder text in text field where user can edit their member label.",
|
|
)
|
|
textField.font = .dynamicTypeBodyClamped
|
|
textField.addTarget(self, action: #selector(textDidChange(_:)), for: .editingChanged)
|
|
textField.delegate = self
|
|
|
|
characterCountLabel.isHidden = true
|
|
if let count = initialMemberLabel?.count {
|
|
characterCountLabel.text = String(Self.maxCharCount - count)
|
|
characterCountLabel.font = .dynamicTypeBody
|
|
characterCountLabel.textColor = UIColor.Signal.tertiaryLabel.withAlphaComponent(0.3)
|
|
characterCountLabel.isHidden = Self.maxCharCount - count > Self.showCharacterCountMax
|
|
characterCountLabel.setContentHuggingHorizontalHigh()
|
|
characterCountLabel.setCompressionResistanceHigh()
|
|
}
|
|
|
|
clearButton.setImage(UIImage(named: "x-circle-fill-compact"), for: .normal)
|
|
clearButton.tintColor = UIColor.Signal.tertiaryLabel
|
|
clearButton.addTarget(self, action: #selector(clearButtonTapped), for: .touchUpInside)
|
|
clearButton.autoSetDimensions(to: .square(16))
|
|
if initialMemberLabel == nil, initialEmoji == nil {
|
|
clearButton.isHidden = true
|
|
}
|
|
|
|
textFieldStack.addArrangedSubview(addEmojiButton)
|
|
textFieldStack.addArrangedSubview(textField)
|
|
textFieldStack.addArrangedSubview(characterCountLabel)
|
|
textFieldStack.addArrangedSubview(clearButton)
|
|
|
|
clearButton.autoPinEdge(.trailing, to: .trailing, of: textFieldStack, withOffset: -16)
|
|
characterCountLabel.autoPinEdge(.trailing, to: .leading, of: clearButton, withOffset: -8)
|
|
|
|
stackView.addArrangedSubview(textFieldStack)
|
|
stackView.setCustomSpacing(34, after: textFieldStack)
|
|
|
|
textFieldStack.translatesAutoresizingMaskIntoConstraints = false
|
|
textFieldStack.heightAnchor.constraint(equalToConstant: 52).isActive = true
|
|
|
|
guard
|
|
let mockConversationItem = buildMockConversationItem(),
|
|
let previewContainer = messageBubblePreviewContainer(renderItem: mockConversationItem)
|
|
else {
|
|
return
|
|
}
|
|
|
|
stackView.addArrangedSubview(previewContainer)
|
|
}
|
|
|
|
private func buildMockConversationItem() -> CVRenderItem? {
|
|
let attachmentContentValidator = DependenciesBridge.shared.attachmentContentValidator
|
|
let messageBody = db.write { tx in
|
|
attachmentContentValidator.truncatedMessageBodyForInlining(
|
|
MessageBody(text: OWSLocalizedString(
|
|
"MEMBER_LABEL_VIEW_MESSAGE_PREVIEW_TEXT",
|
|
comment: "Text shown in the preview message bubble when a user is editing their member label.",
|
|
), ranges: .empty),
|
|
tx: tx,
|
|
)
|
|
}
|
|
|
|
guard
|
|
let secretParams = try? GroupSecretParams.generate()
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
var groupModelBuilder = TSGroupModelBuilder(secretParams: secretParams)
|
|
var groupMembershipBuilder = groupModelBuilder.groupMembership.asBuilder
|
|
if let updatedMemberLabel {
|
|
groupMembershipBuilder.setMemberLabel(label: MemberLabel(label: updatedMemberLabel, labelEmoji: updatedEmoji), aci: localIdentifiers.aci)
|
|
}
|
|
groupModelBuilder.groupMembership = groupMembershipBuilder.build()
|
|
|
|
guard let groupModel = try? groupModelBuilder.buildAsV2() else {
|
|
return nil
|
|
}
|
|
|
|
let mockGroupThread = MockGroupThread(groupModel: groupModel)
|
|
let mockMessage = MockIncomingMessage(messageBody: messageBody, thread: mockGroupThread, authorAci: localIdentifiers.aci)
|
|
|
|
let renderItem = db.read { tx in
|
|
let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(for: mockGroupThread, ignoreMissing: true, transaction: tx)
|
|
|
|
let conversationStyle = ConversationStyle(
|
|
type: .`default`,
|
|
thread: mockGroupThread,
|
|
viewWidth: view.width - 44, // stack view padding
|
|
hasWallpaper: false,
|
|
shouldDimWallpaperInDarkMode: false,
|
|
chatColor: PaletteChatColor.ultramarine.colorSetting,
|
|
)
|
|
|
|
return CVLoader.buildStandaloneRenderItem(
|
|
interaction: mockMessage,
|
|
thread: mockGroupThread,
|
|
threadAssociatedData: threadAssociatedData,
|
|
conversationStyle: conversationStyle,
|
|
spoilerState: SpoilerRenderState(),
|
|
groupNameColors: groupNameColors,
|
|
transaction: tx,
|
|
)
|
|
}
|
|
return renderItem
|
|
}
|
|
|
|
func messageBubblePreviewContainer(renderItem: CVRenderItem) -> UIStackView? {
|
|
let previewTitle = UILabel()
|
|
previewTitle.text = OWSLocalizedString(
|
|
"MEMBER_LABEL_PREVIEW_HEADING",
|
|
comment: "Heading shown above the preview of a message bubble with the edited member label.",
|
|
)
|
|
previewTitle.font = .dynamicTypeBodyClamped.semibold()
|
|
|
|
let cellView = CVCellView()
|
|
cellView.configure(renderItem: renderItem, componentDelegate: self)
|
|
cellView.isCellVisible = true
|
|
cellView.autoSetDimension(.height, toSize: renderItem.cellMeasurement.cellSize.height)
|
|
cellView.autoSetDimension(.width, toSize: renderItem.cellMeasurement.cellSize.width)
|
|
|
|
let cellContainer = UIView()
|
|
cellContainer.layer.cornerRadius = 27
|
|
cellContainer.layer.masksToBounds = true
|
|
cellContainer.backgroundColor = UIColor.Signal.tertiaryBackground
|
|
|
|
cellContainer.addSubview(cellView)
|
|
cellView.autoPinEdge(toSuperviewEdge: .top, withInset: 20)
|
|
cellView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 20)
|
|
|
|
previewContainer = UIStackView()
|
|
previewContainer?.axis = .vertical
|
|
previewContainer?.spacing = 8
|
|
previewContainer?.addArrangedSubview(previewTitle)
|
|
previewContainer?.addArrangedSubview(cellContainer)
|
|
|
|
return previewContainer
|
|
}
|
|
|
|
@objc
|
|
private func clearButtonTapped() {
|
|
textField.text = ""
|
|
updatedMemberLabel = nil
|
|
updatedEmoji = nil
|
|
addEmojiButton.setImage(UIImage(named: "emoji-plus"), for: .normal)
|
|
addEmojiButton.setTitle(nil, for: .normal)
|
|
addEmojiButton.tintColor = UIColor.Signal.secondaryLabel
|
|
|
|
reloadMessagePreview()
|
|
reloadDoneButtonStatus()
|
|
}
|
|
|
|
@objc
|
|
private func didTapDone() {
|
|
Logger.info("")
|
|
var memberLabel: MemberLabel?
|
|
if let updatedMemberLabel {
|
|
memberLabel = MemberLabel(label: updatedMemberLabel, labelEmoji: updatedEmoji)
|
|
}
|
|
dismiss(animated: true, completion: {
|
|
owsAssertDebug(self.updateDelegate != nil)
|
|
self.updateDelegate?.updateLabelForLocalUser(memberLabel: memberLabel)
|
|
self.onDismiss()
|
|
})
|
|
}
|
|
|
|
@objc
|
|
private func didTapEmojiPicker() {
|
|
let picker = EmojiPickerSheet(message: nil, allowReactionConfiguration: false) { [weak self] emoji in
|
|
guard let emojiString = emoji?.rawValue else {
|
|
return
|
|
}
|
|
self?.updatedEmoji = emojiString
|
|
self?.addEmojiButton.setImage(nil, for: .normal)
|
|
self?.addEmojiButton.setTitle(emojiString, for: .normal)
|
|
self?.addEmojiButton.titleLabel?.font = .dynamicTypeTitle3Clamped
|
|
self?.reloadDoneButtonStatus()
|
|
self?.reloadMessagePreview()
|
|
}
|
|
present(picker, animated: true)
|
|
}
|
|
|
|
private func reloadMessagePreview() {
|
|
if let previewContainer {
|
|
stackView.removeArrangedSubview(previewContainer)
|
|
previewContainer.removeFromSuperview()
|
|
}
|
|
previewContainer = nil
|
|
if
|
|
let mockRenderItem = buildMockConversationItem(),
|
|
let previewContainer = messageBubblePreviewContainer(renderItem: mockRenderItem)
|
|
{
|
|
stackView.insertArrangedSubview(previewContainer, at: 2)
|
|
}
|
|
let count = textField.text?.count ?? 0
|
|
let charsRemaining = Self.maxCharCount - count
|
|
characterCountLabel.text = String(charsRemaining)
|
|
characterCountLabel.isHidden = charsRemaining > Self.showCharacterCountMax
|
|
characterCountLabel.textColor = charsRemaining > 5 ? UIColor.Signal.tertiaryLabel.withAlphaComponent(0.3) : UIColor.Signal.red
|
|
|
|
if updatedMemberLabel == nil, updatedEmoji == nil {
|
|
clearButton.isHidden = true
|
|
} else {
|
|
clearButton.isHidden = false
|
|
}
|
|
}
|
|
|
|
private func reloadDoneButtonStatus() {
|
|
// No change, don't allow sending.
|
|
if initialMemberLabel == updatedMemberLabel, initialEmoji == updatedEmoji {
|
|
navigationItem.rightBarButtonItem?.isEnabled = false
|
|
return
|
|
}
|
|
|
|
// Clears member label, this is allowed.
|
|
if updatedMemberLabel == nil, updatedEmoji == nil {
|
|
navigationItem.rightBarButtonItem?.isEnabled = true
|
|
return
|
|
}
|
|
|
|
// Don't allow emoji-only.
|
|
if updatedMemberLabel == nil {
|
|
navigationItem.rightBarButtonItem?.isEnabled = false
|
|
return
|
|
}
|
|
|
|
navigationItem.rightBarButtonItem?.isEnabled = true
|
|
}
|
|
|
|
@objc
|
|
func textDidChange(_ textField: UITextField) {
|
|
let filteredText = textField.text?.filterStringForDisplay()
|
|
let collapsedFilteredText = filteredText?.replacingOccurrences(
|
|
of: "\\s+",
|
|
with: " ",
|
|
options: .regularExpression,
|
|
)
|
|
|
|
updatedMemberLabel = collapsedFilteredText?.nilIfEmpty
|
|
reloadDoneButtonStatus()
|
|
reloadMessagePreview()
|
|
}
|
|
|
|
// MARK: - UITextFieldDelegate
|
|
|
|
func textField(
|
|
_ textField: UITextField,
|
|
shouldChangeCharactersIn range: NSRange,
|
|
replacementString string: String,
|
|
) -> Bool {
|
|
TextFieldHelper.textField(
|
|
textField,
|
|
shouldChangeCharactersInRange: range,
|
|
replacementString: string,
|
|
maxByteCount: 96,
|
|
maxGlyphCount: Self.maxCharCount,
|
|
)
|
|
}
|
|
|
|
// MARK: - Group member list
|
|
|
|
private func sortedMembers() -> [(key: SignalServiceAddress, value: MemberLabelForRendering)] {
|
|
let allMembersSorted = db.read { tx in contactManager.sortSignalServiceAddresses(groupMemberLabelsWithoutLocalUser.keys, transaction: tx)
|
|
}
|
|
|
|
var membersToRender = [SignalServiceAddress]()
|
|
let groupMembership = groupModel.groupMembership
|
|
// Admin users are first.
|
|
let adminMembers = allMembersSorted.filter { groupMembership.isFullMemberAndAdministrator($0) }
|
|
membersToRender += adminMembers
|
|
// Non-admin users are second.
|
|
let nonAdminMembers = allMembersSorted.filter { !groupMembership.isFullMemberAndAdministrator($0) }
|
|
membersToRender += nonAdminMembers
|
|
|
|
return membersToRender.map { (key: $0, value: groupMemberLabelsWithoutLocalUser[$0]!) }
|
|
}
|
|
|
|
private func buildGroupMembershipSection() {
|
|
let contactListStackView = UIStackView()
|
|
contactListStackView.spacing = 5
|
|
contactListStackView.axis = .vertical
|
|
contactListStackView.backgroundColor = UIColor.Signal.tertiaryBackground
|
|
contactListStackView.layer.masksToBounds = true
|
|
contactListStackView.layer.cornerRadius = 26
|
|
contactListStackView.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
|
|
contactListStackView.isLayoutMarginsRelativeArrangement = true
|
|
|
|
let sortedNonLocalMembers = sortedMembers()
|
|
var cellCount = 0
|
|
for (memberAddress, memberLabel) in sortedNonLocalMembers {
|
|
let cell = ContactCellView()
|
|
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
|
var configuration = ContactCellView.Configuration(address: memberAddress, localUserDisplayMode: .asLocalUser)
|
|
|
|
configuration.memberLabel = memberLabel
|
|
|
|
let isSystemContact = SSKEnvironment.shared.contactManagerRef.fetchSignalAccount(
|
|
for: memberAddress,
|
|
transaction: tx,
|
|
) != nil
|
|
|
|
configuration.shouldShowContactIcon = isSystemContact
|
|
cell.configure(configuration: configuration, transaction: tx)
|
|
|
|
if cellCount > 0 {
|
|
let separator = UIView()
|
|
separator.backgroundColor = UIColor.Signal.tertiaryLabel
|
|
contactListStackView.addArrangedSubview(separator)
|
|
NSLayoutConstraint.activate([
|
|
separator.heightAnchor.constraint(equalToConstant: .hairlineWidth),
|
|
])
|
|
contactListStackView.setCustomSpacing(6, after: separator)
|
|
}
|
|
|
|
contactListStackView.addArrangedSubview(cell)
|
|
cellCount += 1
|
|
}
|
|
}
|
|
|
|
if cellCount > 0 {
|
|
contactListStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.addArrangedSubview(contactListStackView)
|
|
NSLayoutConstraint.activate([
|
|
contactListStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
|
|
contactListStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
|
|
])
|
|
self.contactListStackView = contactListStackView
|
|
} else {
|
|
let cellContainer = UIView()
|
|
cellContainer.layer.cornerRadius = 27
|
|
cellContainer.layer.masksToBounds = true
|
|
cellContainer.backgroundColor = UIColor.Signal.tertiaryBackground
|
|
|
|
let noOtherMembersLabel = UILabel()
|
|
noOtherMembersLabel.text = OWSLocalizedString("MEMBER_LABEL_NO_OTHER_GROUP_MEMBERS_HAVE_LABELS", comment: "Text for section that shows other group member labels, when there are none")
|
|
noOtherMembersLabel.font = .dynamicTypeFootnoteClamped
|
|
noOtherMembersLabel.textColor = UIColor.Signal.secondaryLabel
|
|
noOtherMembersLabel.textAlignment = .center
|
|
noOtherMembersLabel.numberOfLines = 0
|
|
cellContainer.addSubview(noOtherMembersLabel)
|
|
noOtherMembersLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
noOtherMembersLabel.centerXAnchor.constraint(equalTo: cellContainer.centerXAnchor),
|
|
noOtherMembersLabel.centerYAnchor.constraint(equalTo: cellContainer.centerYAnchor),
|
|
])
|
|
stackView.addArrangedSubview(cellContainer)
|
|
cellContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
cellContainer.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
|
|
cellContainer.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
|
|
cellContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 96),
|
|
])
|
|
self.noOtherMembersLabelContainer = cellContainer
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
private func dismissKeyboard() {
|
|
view.endEditing(true)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension MemberLabelViewController: CVComponentDelegate {
|
|
var spoilerState: SignalUI.SpoilerRenderState {
|
|
return SpoilerRenderState()
|
|
}
|
|
|
|
func enqueueReload() {}
|
|
|
|
func enqueueReloadWithoutCaches() {}
|
|
|
|
func didTapBodyTextItem(_ item: CVTextLabel.Item) {}
|
|
|
|
func didLongPressBodyTextItem(_ item: CVTextLabel.Item) {}
|
|
|
|
func didTapSystemMessageItem(_ item: CVTextLabel.Item) {}
|
|
|
|
func didTapCollapseSet(collapseSetId: String) {}
|
|
|
|
func didDoubleTapTextViewItem(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didLongPressTextViewItem(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowMessageSendActions: Bool,
|
|
) {}
|
|
|
|
func didLongPressMediaViewItem(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowMessageSendActions: Bool,
|
|
) {}
|
|
|
|
func didLongPressQuote(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowMessageSendActions: Bool,
|
|
) {}
|
|
|
|
func didLongPressSystemMessage(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
) {}
|
|
|
|
func didLongPressSticker(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowMessageSendActions: Bool,
|
|
) {}
|
|
|
|
func didLongPressPaymentMessage(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowMessageSendActions: Bool,
|
|
) {}
|
|
|
|
func didLongPressPoll(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowMessageSendActions: Bool,
|
|
) {}
|
|
|
|
func didTapPayment(_ payment: PaymentsHistoryItem) {}
|
|
|
|
func didChangeLongPress(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didEndLongPress(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didCancelLongPress(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
// MARK: -
|
|
|
|
func willBecomeVisibleWithSkippedDownloads(_ message: TSMessage) {}
|
|
|
|
func didTapSkippedDownloads(_ message: TSMessage) {}
|
|
|
|
func didCancelDownload(_ message: TSMessage, attachmentId: Attachment.IDType) {}
|
|
|
|
// MARK: -
|
|
|
|
func didTapReplyToItem(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didTapSenderAvatar(_ interaction: TSInteraction) {}
|
|
|
|
func shouldAllowMessageSendActionsForItem(_ itemViewModel: CVItemViewModelImpl) -> Bool { false }
|
|
|
|
func didTapReactions(
|
|
reactionState: InteractionReactionState,
|
|
message: TSMessage,
|
|
) {}
|
|
|
|
func didTapTruncatedTextMessage(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didTapShowEditHistory(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
var hasPendingMessageRequest: Bool { false }
|
|
|
|
func didTapUndownloadableMedia() {}
|
|
|
|
func didTapUndownloadableGenericFile() {}
|
|
|
|
func didTapUndownloadableOversizeText() {}
|
|
|
|
func didTapUndownloadableAudio() {}
|
|
|
|
func didTapUndownloadableSticker() {}
|
|
|
|
func didTapBrokenVideo() {}
|
|
|
|
func didTapBodyMedia(
|
|
itemViewModel: CVItemViewModelImpl,
|
|
attachment: ReferencedAttachment,
|
|
imageView: UIView,
|
|
) {}
|
|
|
|
func didTapGenericAttachment(
|
|
_ attachment: CVComponentGenericAttachment,
|
|
) -> CVAttachmentTapAction { .default }
|
|
|
|
func didTapQuotedReply(_ quotedReply: QuotedReplyModel) {}
|
|
|
|
func didTapLinkPreview(url: URL) {}
|
|
|
|
func didTapContactShare(_ contactShare: ContactShareViewModel) {}
|
|
|
|
func didTapSendMessage(to phoneNumbers: [String]) {}
|
|
|
|
func didTapSendInvite(toContactShare contactShare: ContactShareViewModel) {}
|
|
|
|
func didTapAddToContacts(contactShare: ContactShareViewModel) {}
|
|
|
|
func didTapStickerPack(_ stickerPackInfo: StickerPackInfo) {}
|
|
|
|
func didTapGroupInviteLink(url: URL) {}
|
|
|
|
func didTapProxyLink(url: URL) {}
|
|
|
|
func didTapShowMessageDetail(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func willWrapGift(_ messageUniqueId: String) -> Bool { false }
|
|
|
|
func willShakeGift(_ messageUniqueId: String) -> Bool { false }
|
|
|
|
func willUnwrapGift(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didTapGiftBadge(
|
|
_ itemViewModel: CVItemViewModelImpl,
|
|
profileBadge: ProfileBadge,
|
|
isExpired: Bool,
|
|
isRedeemed: Bool,
|
|
) {}
|
|
|
|
func prepareMessageDetailForInteractivePresentation(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func beginCellAnimation(maximumDuration: TimeInterval) -> EndCellAnimation {
|
|
return {}
|
|
}
|
|
|
|
var wallpaperBlurProvider: WallpaperBlurProvider? { nil }
|
|
|
|
var selectionState: CVSelectionState { CVSelectionState() }
|
|
|
|
func didTapPreviouslyVerifiedIdentityChange(_ address: SignalServiceAddress) {}
|
|
|
|
func didTapUnverifiedIdentityChange(_ address: SignalServiceAddress) {}
|
|
|
|
func didTapSessionRefreshMessage(_ message: TSErrorMessage) {}
|
|
|
|
func didTapResendGroupUpdateForErrorMessage(_ errorMessage: TSErrorMessage) {}
|
|
|
|
func didTapIndividualCall(_ call: TSCall) {}
|
|
|
|
func didTapLearnMoreMissedCallFromBlockedContact(_ call: TSCall) {}
|
|
|
|
func didTapGroupCall() {}
|
|
|
|
func didTapPendingOutgoingMessage(_ message: TSOutgoingMessage) {}
|
|
|
|
func didTapFailedMessage(_ message: TSMessage) {}
|
|
|
|
func didTapGroupMigrationLearnMore() {}
|
|
|
|
func didTapGroupInviteLinkPromotion(groupModel: TSGroupModel) {}
|
|
|
|
func didTapViewGroupDescription(newGroupDescription: String) {}
|
|
|
|
func didTapNameEducation(type: SafetyTipsType) {}
|
|
|
|
func didTapShowConversationSettings() {}
|
|
|
|
func didTapShowConversationSettingsAndShowMemberRequests() {}
|
|
|
|
func didTapBlockRequest(
|
|
groupModel: TSGroupModelV2,
|
|
requesterName: String,
|
|
requesterAci: Aci,
|
|
) {}
|
|
|
|
func didTapShowUpgradeAppUI() {}
|
|
|
|
func didTapUpdateSystemContact(
|
|
_ address: SignalServiceAddress,
|
|
newNameComponents: PersonNameComponents,
|
|
) {}
|
|
|
|
func didTapPhoneNumberChange(aci: Aci, phoneNumberOld: String, phoneNumberNew: String) {}
|
|
|
|
func didTapViewOnceAttachment(_ interaction: TSInteraction) {}
|
|
|
|
func didTapViewOnceExpired(_ interaction: TSInteraction) {}
|
|
|
|
func didTapContactName(thread: TSContactThread) {}
|
|
|
|
func didTapUnknownThreadWarningGroup() {}
|
|
func didTapUnknownThreadWarningContact() {}
|
|
func didTapDeliveryIssueWarning(_ message: TSErrorMessage) {}
|
|
|
|
func didTapActivatePayments() {}
|
|
func didTapSendPayment() {}
|
|
|
|
func didTapThreadMergeLearnMore(phoneNumber: String) {}
|
|
|
|
func didTapReportSpamLearnMore() {}
|
|
|
|
func didTapMessageRequestAcceptedOptions() {}
|
|
|
|
func didTapJoinCallLinkCall(callLink: CallLink) {}
|
|
|
|
func didTapViewVotes(poll: OWSPoll) {}
|
|
|
|
func didTapViewPoll(pollInteractionUniqueId: String) {}
|
|
|
|
func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}
|
|
|
|
func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}
|
|
|
|
func didTapSafetyTips() {}
|
|
}
|