607 lines
24 KiB
Swift
607 lines
24 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import LibSignalClient
|
|
|
|
// MARK: - Group updates
|
|
|
|
public enum GroupUpdateSpamReportingMetadata {
|
|
// This update contains information that can be reported as spam.
|
|
case reportable(serverGuid: String)
|
|
// This update is missing necessary information to report as spam.
|
|
// This could be because the information was dropped (as with the round trip to a backup)
|
|
// or the serverGuid is otherwise missing. This information being missing
|
|
// isn't a fatal error, it just means that no attempt will be made to record or report
|
|
// spam based on this group action.
|
|
case unreportable
|
|
// We can't report this update because it came from a group action (create, join, etc.)
|
|
// intiated locally by the user.
|
|
case createdByLocalAction
|
|
// These updates were initiated locally to update the group state.
|
|
// there's no metadata to associate with these updates for reporting purposes.
|
|
case learnedByLocallyInitatedRefresh
|
|
}
|
|
|
|
public extension TSInfoMessage {
|
|
|
|
static func newGroupUpdateInfoMessage(
|
|
timestamp: UInt64,
|
|
spamReportingMetadata: GroupUpdateSpamReportingMetadata,
|
|
groupThread: TSGroupThread,
|
|
updateItems: [PersistableGroupUpdateItem]
|
|
) -> TSInfoMessage {
|
|
owsAssert(!updateItems.isEmpty)
|
|
|
|
var userInfoForNewMessage: [InfoMessageUserInfoKey: Any] = [:]
|
|
|
|
userInfoForNewMessage[.groupUpdateItems] = PersistableGroupUpdateItemsWrapper(updateItems)
|
|
|
|
let spamReportingServerGuid: String? = {
|
|
switch spamReportingMetadata {
|
|
case .reportable(serverGuid: let serverGuid):
|
|
return serverGuid
|
|
case .unreportable, .createdByLocalAction, .learnedByLocallyInitatedRefresh:
|
|
return nil
|
|
}
|
|
}()
|
|
|
|
let infoMessage = TSInfoMessage(
|
|
thread: groupThread,
|
|
timestamp: timestamp,
|
|
serverGuid: spamReportingServerGuid,
|
|
messageType: .typeGroupUpdate,
|
|
infoMessageUserInfo: userInfoForNewMessage
|
|
)
|
|
return infoMessage
|
|
}
|
|
|
|
@objc
|
|
func groupUpdateDescription(transaction tx: SDSAnyReadTransaction) -> NSAttributedString {
|
|
let fallback = DisplayableGroupUpdateItem.genericUpdateByUnknownUser.localizedText
|
|
|
|
guard let localIdentifiers: LocalIdentifiers =
|
|
DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read) else {
|
|
return fallback
|
|
}
|
|
|
|
let updateItems: [DisplayableGroupUpdateItem]
|
|
|
|
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
|
|
|
|
case .nonGroupUpdate:
|
|
return fallback
|
|
|
|
case .legacyRawString(let string):
|
|
return NSAttributedString(string: string)
|
|
|
|
case .newGroup, .modelDiff, .precomputed:
|
|
guard let items = buildGroupUpdateItems(
|
|
localIdentifiers: localIdentifiers,
|
|
tx: tx.asV2Read,
|
|
transformer: { builder, precomputedItems, tx in
|
|
return builder.displayableUpdateItemsForPrecomputed(
|
|
precomputedUpdateItems: precomputedItems,
|
|
localIdentifiers: localIdentifiers,
|
|
tx: tx
|
|
)
|
|
}
|
|
) else {
|
|
return fallback
|
|
}
|
|
|
|
updateItems = items
|
|
}
|
|
|
|
guard let firstUpdateItem = updateItems.first else {
|
|
owsFailBeta("Should never have an empty update items list!")
|
|
return NSAttributedString()
|
|
}
|
|
|
|
let initialString = NSMutableAttributedString(
|
|
attributedString: firstUpdateItem.localizedText
|
|
)
|
|
|
|
return updateItems.dropFirst().reduce(initialString) { partialResult, updateItem in
|
|
partialResult.append("\n")
|
|
partialResult.append(updateItem.localizedText)
|
|
return partialResult
|
|
}
|
|
}
|
|
|
|
func computedGroupUpdateItems(
|
|
localIdentifiers: LocalIdentifiers,
|
|
tx: SDSAnyReadTransaction
|
|
) -> [PersistableGroupUpdateItem]? {
|
|
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
|
|
case .legacyRawString, .nonGroupUpdate:
|
|
return nil
|
|
|
|
case .newGroup, .modelDiff, .precomputed:
|
|
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(
|
|
tx: tx.asV2Read
|
|
) else {
|
|
owsFailDebug("Missing local identifiers!")
|
|
return nil
|
|
}
|
|
|
|
return buildGroupUpdateItems(
|
|
localIdentifiers: localIdentifiers,
|
|
tx: tx.asV2Read,
|
|
transformer: { _, items, _ in return items }
|
|
)
|
|
}
|
|
}
|
|
|
|
func displayableGroupUpdateItems(
|
|
localIdentifiers: LocalIdentifiers,
|
|
tx: SDSAnyReadTransaction
|
|
) -> [DisplayableGroupUpdateItem]? {
|
|
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
|
|
case .legacyRawString, .nonGroupUpdate:
|
|
return nil
|
|
|
|
case .newGroup, .modelDiff, .precomputed:
|
|
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(
|
|
tx: tx.asV2Read
|
|
) else {
|
|
owsFailDebug("Missing local identifiers!")
|
|
return nil
|
|
}
|
|
|
|
return buildGroupUpdateItems(
|
|
localIdentifiers: localIdentifiers,
|
|
tx: tx.asV2Read,
|
|
transformer: { builder, precomputedItems, tx in
|
|
return builder.displayableUpdateItemsForPrecomputed(
|
|
precomputedUpdateItems: precomputedItems,
|
|
localIdentifiers: localIdentifiers,
|
|
tx: tx
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private func buildGroupUpdateItems<T>(
|
|
localIdentifiers: LocalIdentifiers,
|
|
tx: DBReadTransaction,
|
|
transformer: (
|
|
_ builder: GroupUpdateItemBuilder,
|
|
_ precomputedItems: [PersistableGroupUpdateItem],
|
|
_ tx: DBReadTransaction
|
|
) -> [T]
|
|
) -> [T]? {
|
|
lazy var groupUpdateItemBuilder = GroupUpdateItemBuilderImpl(
|
|
contactsManager: GroupUpdateItemBuilderImpl.Wrappers.ContactsManager(
|
|
contactsManager
|
|
),
|
|
recipientDatabaseTable: DependenciesBridge.shared.recipientDatabaseTable
|
|
)
|
|
|
|
let precomputedItems: [PersistableGroupUpdateItem]
|
|
|
|
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
|
|
|
|
case .nonGroupUpdate, .legacyRawString:
|
|
return nil
|
|
|
|
case .precomputed(let precomputedUpdateItems):
|
|
precomputedItems = precomputedUpdateItems.updateItems
|
|
|
|
case let .newGroup(newGroupModel, updateMetadata):
|
|
precomputedItems = groupUpdateItemBuilder.precomputedUpdateItemsForNewGroup(
|
|
newGroupModel: newGroupModel.groupModel,
|
|
newDisappearingMessageToken: newGroupModel.dmToken,
|
|
localIdentifiers: localIdentifiers,
|
|
groupUpdateSource: updateMetadata.source,
|
|
tx: tx
|
|
)
|
|
|
|
case let .modelDiff(oldGroupModel, newGroupModel, updateMetadata):
|
|
precomputedItems = groupUpdateItemBuilder.precomputedUpdateItemsByDiffingModels(
|
|
oldGroupModel: oldGroupModel.groupModel,
|
|
newGroupModel: newGroupModel.groupModel,
|
|
oldDisappearingMessageToken: oldGroupModel.dmToken,
|
|
newDisappearingMessageToken: newGroupModel.dmToken,
|
|
localIdentifiers: localIdentifiers,
|
|
groupUpdateSource: updateMetadata.source,
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
return transformer(
|
|
groupUpdateItemBuilder,
|
|
precomputedItems,
|
|
tx
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public extension TSInfoMessage {
|
|
@objc
|
|
func profileChangeDescription(transaction: SDSAnyReadTransaction) -> String {
|
|
guard let profileChanges = profileChanges,
|
|
let updateDescription = profileChanges.descriptionForUpdate(transaction: transaction) else {
|
|
owsFailDebug("Unexpectedly missing update description for profile change")
|
|
return ""
|
|
}
|
|
|
|
return updateDescription
|
|
}
|
|
|
|
@objc
|
|
func threadMergeDescription(tx: SDSAnyReadTransaction) -> String {
|
|
let displayName = contactThreadDisplayName(tx: tx)
|
|
if let phoneNumber = infoMessageUserInfo?[.threadMergePhoneNumber] as? String {
|
|
let formatString = OWSLocalizedString(
|
|
"THREAD_MERGE_PHONE_NUMBER",
|
|
comment: "A system event shown in a conversation when multiple conversations for the same person have been merged into one. The parameters are replaced with the contact's name (eg John Doe) and their phone number (eg +1 650 555 0100)."
|
|
)
|
|
let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber)
|
|
return String(format: formatString, displayName, formattedPhoneNumber)
|
|
} else {
|
|
let formatString = OWSLocalizedString(
|
|
"THREAD_MERGE_NO_PHONE_NUMBER",
|
|
comment: "A system event shown in a conversation when multiple conversations for the same person have been merged into one. The parameter is replaced with the contact's name (eg John Doe)."
|
|
)
|
|
return String(format: formatString, displayName)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
func sessionSwitchoverDescription(tx: SDSAnyReadTransaction) -> String {
|
|
if let phoneNumber = infoMessageUserInfo?[.sessionSwitchoverPhoneNumber] as? String {
|
|
let displayName = contactThreadDisplayName(tx: tx)
|
|
let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber)
|
|
let formatString = OWSLocalizedString(
|
|
"SESSION_SWITCHOVER_EVENT",
|
|
comment: "If you send a message to a phone number, we might not know the owner of the account. When you later learn the owner of the account, we may show this message. The first parameter is a phone number; the second parameter is the contact's name. Put differently, this message indicates that a phone number belongs to a particular named recipient."
|
|
)
|
|
return String(format: formatString, formattedPhoneNumber, displayName)
|
|
} else {
|
|
let address = TSContactThread.contactAddress(fromThreadId: uniqueThreadId, transaction: tx)
|
|
return TSErrorMessage.safetyNumberChangeDescription(for: address, tx: tx)
|
|
}
|
|
}
|
|
|
|
private func contactThreadDisplayName(tx: SDSAnyReadTransaction) -> String {
|
|
let result: String? = {
|
|
guard let address = TSContactThread.contactAddress(fromThreadId: uniqueThreadId, transaction: tx) else {
|
|
return nil
|
|
}
|
|
return contactsManager.displayName(for: address, tx: tx).resolvedValue()
|
|
}()
|
|
return result ?? OWSLocalizedString("UNKNOWN_USER", comment: "Label indicating an unknown user.")
|
|
}
|
|
|
|
var profileChangeAddress: SignalServiceAddress? {
|
|
return profileChanges?.address
|
|
}
|
|
|
|
var profileChangesOldFullName: String? {
|
|
profileChanges?.oldFullName
|
|
}
|
|
|
|
var profileChangesNewFullName: String? {
|
|
profileChanges?.newFullName
|
|
}
|
|
|
|
var profileChangeNewNameComponents: PersonNameComponents? {
|
|
return profileChanges?.newNameComponents
|
|
}
|
|
|
|
@objc
|
|
static func legacyDisappearingMessageUpdateDescription(
|
|
token newToken: DisappearingMessageToken,
|
|
wasAddedToExistingGroup: Bool,
|
|
updaterName: String?
|
|
) -> String {
|
|
// This might be zero if DMs are not enabled.
|
|
let durationString = String.formatDurationLossless(
|
|
durationSeconds: newToken.durationSeconds
|
|
)
|
|
|
|
if wasAddedToExistingGroup {
|
|
assert(newToken.isEnabled)
|
|
let format = OWSLocalizedString("DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT",
|
|
comment: "Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.")
|
|
return String(format: format, durationString)
|
|
} else if let updaterName = updaterName {
|
|
if newToken.isEnabled {
|
|
let format = OWSLocalizedString("OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION",
|
|
comment: "Info Message when another user enabled disappearing messages. Embeds {{name of other user}} and {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.")
|
|
return String(format: format, updaterName, durationString)
|
|
} else {
|
|
let format = OWSLocalizedString("OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION",
|
|
comment: "Info Message when another user disabled disappearing messages. Embeds {{name of other user}}.")
|
|
return String(format: format, updaterName)
|
|
}
|
|
} else {
|
|
// Changed by localNumber on this device or via synced transcript
|
|
if newToken.isEnabled {
|
|
let format = OWSLocalizedString("YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION",
|
|
comment: "Info Message when you update disappearing messages duration. Embeds a {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context.")
|
|
return String(format: format, durationString)
|
|
} else {
|
|
return OWSLocalizedString("YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION",
|
|
comment: "Info Message when you disabled disappearing messages.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Payments
|
|
|
|
extension TSInfoMessage {
|
|
|
|
private enum PaymentsInfoMessageType {
|
|
case incoming(from: Aci)
|
|
case outgoing(to: Aci)
|
|
}
|
|
|
|
private func paymentsActivationRequestType(transaction: SDSAnyReadTransaction) -> PaymentsInfoMessageType? {
|
|
guard
|
|
let paymentActivationRequestSenderAci,
|
|
let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)?.aci
|
|
else {
|
|
return nil
|
|
}
|
|
if paymentActivationRequestSenderAci == localAci {
|
|
guard let peerAci = TSContactThread.contactAddress(
|
|
fromThreadId: self.uniqueThreadId,
|
|
transaction: transaction
|
|
)?.aci else {
|
|
return nil
|
|
}
|
|
return .outgoing(to: peerAci)
|
|
} else {
|
|
return .incoming(from: paymentActivationRequestSenderAci)
|
|
}
|
|
}
|
|
|
|
private func paymentsActivatedType(transaction: SDSAnyReadTransaction) -> PaymentsInfoMessageType? {
|
|
guard
|
|
let paymentActivatedAci,
|
|
let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)?.aci
|
|
else {
|
|
return nil
|
|
}
|
|
if paymentActivatedAci == localAci {
|
|
guard let peerAci = TSContactThread.contactAddress(
|
|
fromThreadId: self.uniqueThreadId,
|
|
transaction: transaction
|
|
)?.aci else {
|
|
return nil
|
|
}
|
|
return .outgoing(to: peerAci)
|
|
} else {
|
|
return .incoming(from: paymentActivatedAci)
|
|
}
|
|
}
|
|
|
|
public func isIncomingPaymentsActivationRequest(_ tx: SDSAnyReadTransaction) -> Bool {
|
|
switch paymentsActivationRequestType(transaction: tx) {
|
|
case .none, .outgoing:
|
|
return false
|
|
case .incoming:
|
|
return true
|
|
}
|
|
}
|
|
|
|
public func isIncomingPaymentsActivated(_ tx: SDSAnyReadTransaction) -> Bool {
|
|
switch paymentsActivatedType(transaction: tx) {
|
|
case .none, .outgoing:
|
|
return false
|
|
case .incoming:
|
|
return true
|
|
}
|
|
}
|
|
|
|
@objc
|
|
func paymentsActivationRequestDescription(transaction: SDSAnyReadTransaction) -> String? {
|
|
let aci: Aci
|
|
let formatString: String
|
|
switch paymentsActivationRequestType(transaction: transaction) {
|
|
case .none:
|
|
return nil
|
|
case .incoming(let fromAci):
|
|
aci = fromAci
|
|
formatString = OWSLocalizedString(
|
|
"INFO_MESSAGE_PAYMENTS_ACTIVATION_REQUEST_RECEIVED",
|
|
comment: "Shown when a user receives a payment activation request. Embeds: {{ the user's name}}"
|
|
)
|
|
case .outgoing(let toAci):
|
|
aci = toAci
|
|
formatString = OWSLocalizedString(
|
|
"INFO_MESSAGE_PAYMENTS_ACTIVATION_REQUEST_SENT",
|
|
comment: "Shown when requesting a user activates payments. Embeds: {{ the user's name}}"
|
|
)
|
|
}
|
|
|
|
let displayName = contactsManager.displayName(for: SignalServiceAddress(aci), tx: transaction)
|
|
return String(format: formatString, displayName.resolvedValue())
|
|
}
|
|
|
|
@objc
|
|
func paymentsActivatedDescription(transaction: SDSAnyReadTransaction) -> String? {
|
|
switch paymentsActivatedType(transaction: transaction) {
|
|
case .none:
|
|
return nil
|
|
case .outgoing:
|
|
return OWSLocalizedString(
|
|
"INFO_MESSAGE_PAYMENTS_ACTIVATED",
|
|
comment: "Shown when a user activates payments from a chat"
|
|
)
|
|
case .incoming(let aci):
|
|
let displayName = contactsManager.displayName(for: SignalServiceAddress(aci), tx: transaction)
|
|
let format = OWSLocalizedString(
|
|
"INFO_MESSAGE_PAYMENTS_ACTIVATION_REQUEST_FINISHED",
|
|
comment: "Shown when a user activates payments from a chat. Embeds: {{ the user's name}}"
|
|
)
|
|
return String(format: format, displayName.resolvedValue())
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - InfoMessageUserInfo
|
|
|
|
extension TSInfoMessage {
|
|
|
|
private func infoMessageValue<T>(forKey key: InfoMessageUserInfoKey) -> T? {
|
|
guard let infoMessageUserInfo = self.infoMessageUserInfo else {
|
|
return nil
|
|
}
|
|
|
|
guard let groupModel = infoMessageUserInfo[key] as? T else {
|
|
assert(infoMessageUserInfo[key] == nil)
|
|
return nil
|
|
}
|
|
|
|
return groupModel
|
|
}
|
|
|
|
public enum GroupUpdateMetadata {
|
|
public struct UpdateMetadata {
|
|
public let source: GroupUpdateSource
|
|
/// Whether we determined, at the time we created this info message, that
|
|
/// the updater was the local user.
|
|
/// - Returns
|
|
/// `true` if we knew conclusively that the updater was the local user, and
|
|
/// `false` otherwise.
|
|
public let updaterWasLocalUser: Bool
|
|
}
|
|
|
|
public struct GroupModel {
|
|
public let groupModel: TSGroupModel
|
|
public let dmToken: DisappearingMessageToken?
|
|
}
|
|
|
|
/// For legacy group updates we persisted a pre-rendered string, rather than the details
|
|
/// to generate that string.
|
|
case legacyRawString(String)
|
|
|
|
// For some time after we would persist the group state before and after
|
|
// (or just the new group state) along with other optional metadata.
|
|
// This will be soon unused at write time, but can still be present
|
|
// in the database as there was no migration done.
|
|
case newGroup(GroupModel, updateMetadata: UpdateMetadata)
|
|
case modelDiff(old: GroupModel, new: GroupModel, updateMetadata: UpdateMetadata)
|
|
|
|
/// Modern group updates are precomputed into an enum and stored with all necessary metadata
|
|
/// strongly typed, whether its a new group or an update to an existing group.
|
|
/// Group state is NOT stored with these types.
|
|
case precomputed(PersistableGroupUpdateItemsWrapper)
|
|
|
|
/// This is not a group update.
|
|
case nonGroupUpdate
|
|
}
|
|
|
|
public func groupUpdateMetadata(localIdentifiers: LocalIdentifiers) -> GroupUpdateMetadata {
|
|
if let precomputed: PersistableGroupUpdateItemsWrapper =
|
|
infoMessageValue(forKey: .groupUpdateItems)
|
|
{
|
|
return .precomputed(precomputed)
|
|
} else if let legacyPrecomputed: LegacyPersistableGroupUpdateItemsWrapper =
|
|
infoMessageValue(forKey: .legacyGroupUpdateItems)
|
|
{
|
|
let updateMetadata = self.persistedLegacyUpdateMetadata
|
|
// Convert the legacy items into new items.
|
|
let mappedItems: [PersistableGroupUpdateItem] = legacyPrecomputed
|
|
.updateItems
|
|
.compactMap { legacyItem in
|
|
return legacyItem.toNewItem(
|
|
updater: updateMetadata.source,
|
|
oldGroupModel: infoMessageValue(forKey: .oldGroupModel),
|
|
localIdentifiers: localIdentifiers
|
|
)
|
|
}
|
|
return .precomputed(.init(mappedItems))
|
|
} else if
|
|
let newGroupModel: TSGroupModel = infoMessageValue(forKey: .newGroupModel)
|
|
{
|
|
let updateMetadata = self.persistedLegacyUpdateMetadata
|
|
|
|
if let oldGroupModel: TSGroupModel = infoMessageValue(forKey: .oldGroupModel) {
|
|
return .modelDiff(
|
|
old: .init(
|
|
groupModel: oldGroupModel,
|
|
dmToken: infoMessageValue(forKey: .oldDisappearingMessageToken)
|
|
),
|
|
new: .init(
|
|
groupModel: newGroupModel,
|
|
dmToken: infoMessageValue(forKey: .newDisappearingMessageToken)
|
|
),
|
|
updateMetadata: updateMetadata
|
|
)
|
|
} else {
|
|
return .newGroup(
|
|
.init(
|
|
groupModel: newGroupModel,
|
|
dmToken: infoMessageValue(forKey: .newDisappearingMessageToken)
|
|
),
|
|
updateMetadata: updateMetadata
|
|
)
|
|
}
|
|
} else if let customMessage {
|
|
return .legacyRawString(customMessage)
|
|
} else {
|
|
if messageType == .typeGroupUpdate {
|
|
owsFailDebug("Group update should contain some metadata!")
|
|
}
|
|
return .nonGroupUpdate
|
|
}
|
|
}
|
|
|
|
/// We only stored this legacy data before we persisted the new ``TSInfoMessage.PersistableGroupUpdateItem``,
|
|
/// so it lives either alongside a model diff or alongside ``TSInfoMessage.LegacyPersistableGroupUpdateItem``.
|
|
private var persistedLegacyUpdateMetadata: GroupUpdateMetadata.UpdateMetadata {
|
|
let source = Self.legacyGroupUpdateSource(infoMessageUserInfoDict: infoMessageUserInfo)
|
|
// We grab this legacy value if we have it; its irrelevant for new persistable
|
|
// update items which know if they are from the local user or not.
|
|
let updaterWasLocalUser: Bool =
|
|
infoMessageValue(forKey: .legacyUpdaterKnownToBeLocalUser) ?? false
|
|
|
|
return GroupUpdateMetadata.UpdateMetadata(
|
|
source: source,
|
|
updaterWasLocalUser: updaterWasLocalUser
|
|
)
|
|
}
|
|
|
|
fileprivate var profileChanges: ProfileChanges? {
|
|
return infoMessageValue(forKey: .profileChanges)
|
|
}
|
|
|
|
fileprivate var paymentActivationRequestSenderAci: Aci? {
|
|
guard let raw: String = infoMessageValue(forKey: .paymentActivationRequestSenderAci) else {
|
|
return nil
|
|
}
|
|
return try? Aci.parseFrom(serviceIdString: raw)
|
|
}
|
|
|
|
fileprivate var paymentActivatedAci: Aci? {
|
|
guard let raw: String = infoMessageValue(forKey: .paymentActivatedAci) else {
|
|
return nil
|
|
}
|
|
return try? Aci.parseFrom(serviceIdString: raw)
|
|
}
|
|
}
|
|
|
|
extension TSInfoMessage {
|
|
private func setInfoMessageValue(_ value: Any, forKey key: InfoMessageUserInfoKey) {
|
|
if self.infoMessageUserInfo != nil {
|
|
self.infoMessageUserInfo![key] = value
|
|
} else {
|
|
self.infoMessageUserInfo = [key: value]
|
|
}
|
|
}
|
|
|
|
public func setGroupUpdateItemsWrapper(_ updateItemsWrapper: PersistableGroupUpdateItemsWrapper) {
|
|
setInfoMessageValue(updateItemsWrapper, forKey: .groupUpdateItems)
|
|
}
|
|
}
|