From f3549a939fea64cc0e4b94d84459f4530b1ed88c Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 16 Oct 2026 17:01:38 -0400 Subject: [PATCH 1/2] chore(contract): sync flipcash2 protos to 733ea7f9 --- .../ContractInfo.swift | 2 +- .../activity_v1_model.pb.swift | 137 +++++++++- .../chat_v1_model.pb.swift | 133 ++++++++- .../community_v1_community_service.grpc.swift | 227 ++++++++++++++++ .../community_v1_community_service.pb.swift | 224 ++++++++++++++++ .../community_v1_model.pb.swift | 253 ++++++++++++++++++ .../intent_v1_model.pb.swift | 138 +++++++++- .../messaging_v1_messaging_service.pb.swift | 38 ++- .../messaging_v1_model.pb.swift | 117 ++++++-- flipcash2.lock | 4 +- proto/activity/v1/model.proto | 29 ++ proto/chat/v1/model.proto | 43 +++ proto/community/v1/community_service.proto | 51 ++++ proto/community/v1/model.proto | 74 +++++ proto/intent/v1/model.proto | 48 ++++ proto/messaging/v1/messaging_service.proto | 38 ++- proto/messaging/v1/model.proto | 85 ++++-- 17 files changed, 1576 insertions(+), 65 deletions(-) create mode 100644 Sources/Flipcash2ClientProtocol/community_v1_community_service.grpc.swift create mode 100644 Sources/Flipcash2ClientProtocol/community_v1_community_service.pb.swift create mode 100644 Sources/Flipcash2ClientProtocol/community_v1_model.pb.swift create mode 100644 proto/community/v1/community_service.proto create mode 100644 proto/community/v1/model.proto diff --git a/Sources/Flipcash2ClientProtocol/ContractInfo.swift b/Sources/Flipcash2ClientProtocol/ContractInfo.swift index a787f99..ac8f667 100644 --- a/Sources/Flipcash2ClientProtocol/ContractInfo.swift +++ b/Sources/Flipcash2ClientProtocol/ContractInfo.swift @@ -2,7 +2,7 @@ public enum Flipcash2ContractInfo { public static let version = "0.19.0-dev" - public static let protoCommit = "1897108ae75e032fc5f7de13cf6e9cadcec9deda" + public static let protoCommit = "733ea7f9f33291b9631f0176fab5485acf161fab" public static var isLocal: Bool { protoCommit == localSentinel } diff --git a/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift index 7b204ae..80f3501 100644 --- a/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift @@ -336,6 +336,19 @@ public struct Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: Senda set {destinationIdentifier = .userID(newValue)} } + /// Set when the payment boosted a reaction (see BoostedReaction). A boost + /// that paid one person names them in destination_identifier; one that + /// paid both the message's sender and the group's creator names neither, + /// and payment_amount is the whole boost. + public var boostedReaction: Flipcash_Activity_V1_BoostedReaction { + get {return _boostedReaction ?? Flipcash_Activity_V1_BoostedReaction()} + set {_boostedReaction = newValue} + } + /// Returns true if `boostedReaction` has been explicitly set. + public var hasBoostedReaction: Bool {return self._boostedReaction != nil} + /// Clears the value of `boostedReaction`. Subsequent reads from it will return its default value. + public mutating func clearBoostedReaction() {self._boostedReaction = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_DestinationIdentifier: Equatable, Sendable { @@ -345,6 +358,8 @@ public struct Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: Senda } public init() {} + + fileprivate var _boostedReaction: Flipcash_Activity_V1_BoostedReaction? = nil } public struct Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: Sendable { @@ -370,6 +385,19 @@ public struct Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: Sendable set {sourceIdentifier = .userID(newValue)} } + /// Set when the payment was the recipient's share of a boost of a + /// reaction (see BoostedReaction), paid to them as the boosted message's + /// sender or the group's creator. source_identifier names the payer, and + /// payment_amount is the recipient's share alone. + public var boostedReaction: Flipcash_Activity_V1_BoostedReaction { + get {return _boostedReaction ?? Flipcash_Activity_V1_BoostedReaction()} + set {_boostedReaction = newValue} + } + /// Returns true if `boostedReaction` has been explicitly set. + public var hasBoostedReaction: Bool {return self._boostedReaction != nil} + /// Clears the value of `boostedReaction`. Subsequent reads from it will return its default value. + public mutating func clearBoostedReaction() {self._boostedReaction = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_SourceIdentifier: Equatable, Sendable { @@ -379,6 +407,57 @@ public struct Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: Sendable } public init() {} + + fileprivate var _boostedReaction: Flipcash_Activity_V1_BoostedReaction? = nil +} + +/// The reaction a payment boosted: the payer's reaction with emoji on a +/// message in a group chat (see intent.v1.ChatMetadata.ReactionBoost). It is +/// what the payment was for when it was made and is not kept up to date: the +/// message may since have been deleted, and the viewer may no longer be a +/// member of the chat. +public struct Flipcash_Activity_V1_BoostedReaction: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The group chat the boosted message is in. + public var chatID: Flipcash_Common_V1_ChatId { + get {return _chatID ?? Flipcash_Common_V1_ChatId()} + set {_chatID = newValue} + } + /// Returns true if `chatID` has been explicitly set. + public var hasChatID: Bool {return self._chatID != nil} + /// Clears the value of `chatID`. Subsequent reads from it will return its default value. + public mutating func clearChatID() {self._chatID = nil} + + /// The message whose reaction was boosted. + public var messageID: Flipcash_Messaging_V1_MessageId { + get {return _messageID ?? Flipcash_Messaging_V1_MessageId()} + set {_messageID = newValue} + } + /// Returns true if `messageID` has been explicitly set. + public var hasMessageID: Bool {return self._messageID != nil} + /// Clears the value of `messageID`. Subsequent reads from it will return its default value. + public mutating func clearMessageID() {self._messageID = nil} + + /// The emoji of the boosted reaction. + public var emoji: Flipcash_Messaging_V1_Emoji { + get {return _emoji ?? Flipcash_Messaging_V1_Emoji()} + set {_emoji = newValue} + } + /// Returns true if `emoji` has been explicitly set. + public var hasEmoji: Bool {return self._emoji != nil} + /// Clears the value of `emoji`. Subsequent reads from it will return its default value. + public mutating func clearEmoji() {self._emoji = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil + fileprivate var _messageID: Flipcash_Messaging_V1_MessageId? = nil + fileprivate var _emoji: Flipcash_Messaging_V1_Emoji? = nil } public struct Flipcash_Activity_V1_WithdrewCryptoNotificationMetadata: Sendable { @@ -837,7 +916,7 @@ extension Flipcash_Activity_V1_Notification: SwiftProtobuf.Message, SwiftProtobu extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DirectlySentCryptoNotificationMetadata" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0\u{3}boosted_reaction\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -871,6 +950,7 @@ extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProt self.destinationIdentifier = .userID(v) } }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._boostedReaction) }() default: break } } @@ -892,11 +972,15 @@ extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProt }() case nil: break } + try { if let v = self._boostedReaction { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata, rhs: Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata) -> Bool { if lhs.destinationIdentifier != rhs.destinationIdentifier {return false} + if lhs._boostedReaction != rhs._boostedReaction {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -904,7 +988,7 @@ extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProt extension Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ReceivedCryptoNotificationMetadata" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0\u{3}boosted_reaction\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -938,6 +1022,7 @@ extension Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: SwiftProtobuf self.sourceIdentifier = .userID(v) } }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._boostedReaction) }() default: break } } @@ -959,11 +1044,59 @@ extension Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: SwiftProtobuf }() case nil: break } + try { if let v = self._boostedReaction { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata, rhs: Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata) -> Bool { if lhs.sourceIdentifier != rhs.sourceIdentifier {return false} + if lhs._boostedReaction != rhs._boostedReaction {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Activity_V1_BoostedReaction: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".BoostedReaction" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}message_id\0\u{1}emoji\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._chatID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._messageID) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._emoji) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._chatID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._messageID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._emoji { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Activity_V1_BoostedReaction, rhs: Flipcash_Activity_V1_BoostedReaction) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs._messageID != rhs._messageID {return false} + if lhs._emoji != rhs._emoji {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift index 22d5891..6478c93 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift @@ -272,6 +272,26 @@ public struct Flipcash_Chat_V1_Metadata: @unchecked Sendable { /// Clears the value of `coverPicture`. Subsequent reads from it will return its default value. public mutating func clearCoverPicture() {_uniqueStorage()._coverPicture = nil} + /// What the group's members hold of the currency the group is gated on. + /// Only supported for public group chats gated on a single currency + /// launched on Flipcash (see HoldingsSummary for what that means). Unset + /// for every other chat, for such a group until the server has first + /// computed it, and whenever too few of its members hold the currency: + /// the server sets a minimum number of holders (and a positive total) + /// below which it shows no summary, and may change it. A group whose + /// holders fall below the minimum loses its summary at the next refresh. + /// + /// A periodic snapshot (see HoldingsSummary.as_of), not live: no event + /// announces a change, and a client refetches the chat to refresh it. + public var holdingsSummary: Flipcash_Chat_V1_HoldingsSummary { + get {return _storage._holdingsSummary ?? Flipcash_Chat_V1_HoldingsSummary()} + set {_uniqueStorage()._holdingsSummary = newValue} + } + /// Returns true if `holdingsSummary` has been explicitly set. + public var hasHoldingsSummary: Bool {return _storage._holdingsSummary != nil} + /// Clears the value of `holdingsSummary`. Subsequent reads from it will return its default value. + public mutating func clearHoldingsSummary() {_uniqueStorage()._holdingsSummary = nil} + /// Whether messages in this chat are end-to-end encrypted (see /// messaging.v1.EncryptedContent). Only supported for DMs (CONTACT_DM or DM); /// always false for group chats. A private group is always end-to-end @@ -674,6 +694,61 @@ public struct Flipcash_Chat_V1_RosterSummary: Sendable { public init() {} } +/// HoldingsSummary is what a group's members held of the currency the group is +/// gated on, as of one moment (see Metadata.holdings_summary). Holdings are +/// counted for the members at that moment, whether or not each still meets +/// the group's minimum balance requirements. +/// +/// A group is gated on a single currency when its minimum balance +/// requirements (see MinimumBalanceRequirement), listener and speaker alike, +/// all name the same one mint, and that mint is a currency launched on +/// Flipcash. The fiat currency a requirement is denominated in does not +/// matter. A group with no minimum balance requirement is not gated on a +/// currency, nor is one with a requirement that names no mint (and so applies +/// to every mint), nor one whose requirements name different mints. +public struct Flipcash_Chat_V1_HoldingsSummary: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The mint of the currency the group is gated on: the one mint its + /// minimum balance requirements name. + public var mint: Flipcash_Common_V1_PublicKey { + get {return _mint ?? Flipcash_Common_V1_PublicKey()} + set {_mint = newValue} + } + /// Returns true if `mint` has been explicitly set. + public var hasMint: Bool {return self._mint != nil} + /// Clears the value of `mint`. Subsequent reads from it will return its default value. + public mutating func clearMint() {self._mint = nil} + + /// The total amount of the currency held by the group's members, in the + /// mint's quarks. Clients value it in fiat with the currency's price they + /// already show. + public var quarks: UInt64 = 0 + + /// The number of the group's members who held any of the currency. + public var holderCount: UInt64 = 0 + + /// When quarks and holder_count were computed. How often the server + /// recomputes them is its to decide and may change. + public var asOf: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _asOf ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_asOf = newValue} + } + /// Returns true if `asOf` has been explicitly set. + public var hasAsOf: Bool {return self._asOf != nil} + /// Clears the value of `asOf`. Subsequent reads from it will return its default value. + public mutating func clearAsOf() {self._asOf = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _mint: Flipcash_Common_V1_PublicKey? = nil + fileprivate var _asOf: SwiftProtobuf.Google_Protobuf_Timestamp? = nil +} + public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -1576,7 +1651,7 @@ extension Flipcash_Chat_V1_ChatType: SwiftProtobuf._ProtoNameProviding { extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Metadata" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}type\0\u{1}members\0\u{3}last_message\0\u{3}last_activity\0\u{3}latest_event_sequence\0\u{3}is_hidden\0\u{1}title\0\u{3}profile_picture\0\u{3}roster_summary\0\u{1}rules\0\u{3}viewer_state\0\u{1}creator\0\u{3}is_private\0\u{3}in_lobby\0\u{1}description\0\u{3}cover_picture\0\u{4}S\u{1}use_e2ee\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}type\0\u{1}members\0\u{3}last_message\0\u{3}last_activity\0\u{3}latest_event_sequence\0\u{3}is_hidden\0\u{1}title\0\u{3}profile_picture\0\u{3}roster_summary\0\u{1}rules\0\u{3}viewer_state\0\u{1}creator\0\u{3}is_private\0\u{3}in_lobby\0\u{1}description\0\u{3}cover_picture\0\u{3}holdings_summary\0\u{4}R\u{1}use_e2ee\0") fileprivate class _StorageClass { var _chatID: Flipcash_Common_V1_ChatId? = nil @@ -1596,6 +1671,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa var _inLobby: Bool = false var _description_p: String = String() var _coverPicture: Flipcash_Blob_V1_Media? = nil + var _holdingsSummary: Flipcash_Chat_V1_HoldingsSummary? = nil var _useE2Ee: Bool = false // This property is used as the initial default value for new instances of the type. @@ -1624,6 +1700,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa _inLobby = source._inLobby _description_p = source._description_p _coverPicture = source._coverPicture + _holdingsSummary = source._holdingsSummary _useE2Ee = source._useE2Ee } } @@ -1660,6 +1737,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa case 15: try { try decoder.decodeSingularBoolField(value: &_storage._inLobby) }() case 16: try { try decoder.decodeSingularStringField(value: &_storage._description_p) }() case 17: try { try decoder.decodeSingularMessageField(value: &_storage._coverPicture) }() + case 18: try { try decoder.decodeSingularMessageField(value: &_storage._holdingsSummary) }() case 100: try { try decoder.decodeSingularBoolField(value: &_storage._useE2Ee) }() default: break } @@ -1724,6 +1802,9 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = _storage._coverPicture { try visitor.visitSingularMessageField(value: v, fieldNumber: 17) } }() + try { if let v = _storage._holdingsSummary { + try visitor.visitSingularMessageField(value: v, fieldNumber: 18) + } }() if _storage._useE2Ee != false { try visitor.visitSingularBoolField(value: _storage._useE2Ee, fieldNumber: 100) } @@ -1753,6 +1834,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa if _storage._inLobby != rhs_storage._inLobby {return false} if _storage._description_p != rhs_storage._description_p {return false} if _storage._coverPicture != rhs_storage._coverPicture {return false} + if _storage._holdingsSummary != rhs_storage._holdingsSummary {return false} if _storage._useE2Ee != rhs_storage._useE2Ee {return false} return true } @@ -2356,6 +2438,55 @@ extension Flipcash_Chat_V1_RosterSummary: SwiftProtobuf.Message, SwiftProtobuf._ } } +extension Flipcash_Chat_V1_HoldingsSummary: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HoldingsSummary" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}mint\0\u{1}quarks\0\u{3}holder_count\0\u{3}as_of\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._mint) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.quarks) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.holderCount) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._asOf) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._mint { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.quarks != 0 { + try visitor.visitSingularUInt64Field(value: self.quarks, fieldNumber: 2) + } + if self.holderCount != 0 { + try visitor.visitSingularUInt64Field(value: self.holderCount, fieldNumber: 3) + } + try { if let v = self._asOf { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_HoldingsSummary, rhs: Flipcash_Chat_V1_HoldingsSummary) -> Bool { + if lhs._mint != rhs._mint {return false} + if lhs.quarks != rhs.quarks {return false} + if lhs.holderCount != rhs.holderCount {return false} + if lhs._asOf != rhs._asOf {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".MetadataUpdate" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}full_refresh\0\u{3}last_activity_changed\0\u{3}viewer_state_changed\0\u{3}title_changed\0\u{3}profile_picture_changed\0\u{3}description_changed\0\u{3}cover_picture_changed\0") diff --git a/Sources/Flipcash2ClientProtocol/community_v1_community_service.grpc.swift b/Sources/Flipcash2ClientProtocol/community_v1_community_service.grpc.swift new file mode 100644 index 0000000..91bb47b --- /dev/null +++ b/Sources/Flipcash2ClientProtocol/community_v1_community_service.grpc.swift @@ -0,0 +1,227 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: community/v1/community_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +// MARK: - flipcash.community.v1.Community + +/// Namespace containing generated types for the "flipcash.community.v1.Community" service. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public enum Flipcash_Community_V1_Community { + /// Service descriptor for the "flipcash.community.v1.Community" service. + public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.community.v1.Community") + /// Namespace for method metadata. + public enum Method { + /// Namespace for "GetCommunity" metadata. + public enum GetCommunity { + /// Request type for "GetCommunity". + public typealias Input = Flipcash_Community_V1_GetCommunityRequest + /// Response type for "GetCommunity". + public typealias Output = Flipcash_Community_V1_GetCommunityResponse + /// Descriptor for "GetCommunity". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.community.v1.Community"), + method: "GetCommunity" + ) + } + /// Descriptors for all methods in the "flipcash.community.v1.Community" service. + public static let descriptors: [GRPCCore.MethodDescriptor] = [ + GetCommunity.descriptor + ] + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "flipcash.community.v1.Community" service. + public static let flipcash_community_v1_Community = GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.community.v1.Community") +} + +// MARK: flipcash.community.v1.Community (client) + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Flipcash_Community_V1_Community { + /// Generated client protocol for the "flipcash.community.v1.Community" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > Community serves what Flipcash shows about the community around a currency + /// > launched on Flipcash (see CommunityMetadata). + /// > + /// > Everything it returns is public and the same for every viewer. + public protocol ClientProtocol: Sendable { + /// Call the "GetCommunity" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetCommunity returns a currency's community. + /// > + /// > Auth is optional; when set it must be valid, but it changes nothing + /// > about what is returned. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Community_V1_GetCommunityRequest` message. + /// - serializer: A serializer for `Flipcash_Community_V1_GetCommunityRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Community_V1_GetCommunityResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getCommunity( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "flipcash.community.v1.Community" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > Community serves what Flipcash shows about the community around a currency + /// > launched on Flipcash (see CommunityMetadata). + /// > + /// > Everything it returns is public and the same for every viewer. + public struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + public init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "GetCommunity" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetCommunity returns a currency's community. + /// > + /// > Auth is optional; when set it must be valid, but it changes nothing + /// > about what is returned. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Community_V1_GetCommunityRequest` message. + /// - serializer: A serializer for `Flipcash_Community_V1_GetCommunityRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Community_V1_GetCommunityResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func getCommunity( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Flipcash_Community_V1_Community.Method.GetCommunity.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Flipcash_Community_V1_Community.ClientProtocol { + /// Call the "GetCommunity" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetCommunity returns a currency's community. + /// > + /// > Auth is optional; when set it must be valid, but it changes nothing + /// > about what is returned. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Community_V1_GetCommunityRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func getCommunity( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getCommunity( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } +} + +// Helpers providing sugared APIs for 'ClientProtocol' methods. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Flipcash_Community_V1_Community.ClientProtocol { + /// Call the "GetCommunity" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetCommunity returns a currency's community. + /// > + /// > Auth is optional; when set it must be valid, but it changes nothing + /// > about what is returned. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func getCommunity( + _ message: Flipcash_Community_V1_GetCommunityRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getCommunity( + request: request, + options: options, + onResponse: handleResponse + ) + } +} \ No newline at end of file diff --git a/Sources/Flipcash2ClientProtocol/community_v1_community_service.pb.swift b/Sources/Flipcash2ClientProtocol/community_v1_community_service.pb.swift new file mode 100644 index 0000000..226919a --- /dev/null +++ b/Sources/Flipcash2ClientProtocol/community_v1_community_service.pb.swift @@ -0,0 +1,224 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: community/v1/community_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +public struct Flipcash_Community_V1_GetCommunityRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The community to fetch. Only a mint is supported for now; the oneof + /// leaves room for other identifiers. + public var identifier: Flipcash_Community_V1_GetCommunityRequest.OneOf_Identifier? = nil + + /// The mint of the community's currency. + public var mint: Flipcash_Common_V1_PublicKey { + get { + if case .mint(let v)? = identifier {return v} + return Flipcash_Common_V1_PublicKey() + } + set {identifier = .mint(newValue)} + } + + /// Optional. When set it must be valid, but the community is the same with + /// or without it (see Community.GetCommunity). + public var auth: Flipcash_Common_V1_Auth { + get {return _auth ?? Flipcash_Common_V1_Auth()} + set {_auth = newValue} + } + /// Returns true if `auth` has been explicitly set. + public var hasAuth: Bool {return self._auth != nil} + /// Clears the value of `auth`. Subsequent reads from it will return its default value. + public mutating func clearAuth() {self._auth = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The community to fetch. Only a mint is supported for now; the oneof + /// leaves room for other identifiers. + public enum OneOf_Identifier: Equatable, Sendable { + /// The mint of the community's currency. + case mint(Flipcash_Common_V1_PublicKey) + + } + + public init() {} + + fileprivate var _auth: Flipcash_Common_V1_Auth? = nil +} + +public struct Flipcash_Community_V1_GetCommunityResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var result: Flipcash_Community_V1_GetCommunityResponse.Result = .ok + + /// Set only when result == OK. + public var community: Flipcash_Community_V1_CommunityMetadata { + get {return _community ?? Flipcash_Community_V1_CommunityMetadata()} + set {_community = newValue} + } + /// Returns true if `community` has been explicitly set. + public var hasCommunity: Bool {return self._community != nil} + /// Clears the value of `community`. Subsequent reads from it will return its default value. + public mutating func clearCommunity() {self._community = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum Result: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case ok // = 0 + + /// The mint is not a currency launched on Flipcash, or is not yet + /// available. + case notFound // = 1 + case UNRECOGNIZED(Int) + + public init() { + self = .ok + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .notFound + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .ok: return 0 + case .notFound: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Flipcash_Community_V1_GetCommunityResponse.Result] = [ + .ok, + .notFound, + ] + + } + + public init() {} + + fileprivate var _community: Flipcash_Community_V1_CommunityMetadata? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "flipcash.community.v1" + +extension Flipcash_Community_V1_GetCommunityRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".GetCommunityRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}mint\0\u{2}\u{9}auth\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Flipcash_Common_V1_PublicKey? + var hadOneofValue = false + if let current = self.identifier { + hadOneofValue = true + if case .mint(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.identifier = .mint(v) + } + }() + case 10: try { try decoder.decodeSingularMessageField(value: &self._auth) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if case .mint(let v)? = self.identifier { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._auth { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Community_V1_GetCommunityRequest, rhs: Flipcash_Community_V1_GetCommunityRequest) -> Bool { + if lhs.identifier != rhs.identifier {return false} + if lhs._auth != rhs._auth {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Community_V1_GetCommunityResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".GetCommunityResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{1}community\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.result) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._community) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.result != .ok { + try visitor.visitSingularEnumField(value: self.result, fieldNumber: 1) + } + try { if let v = self._community { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Community_V1_GetCommunityResponse, rhs: Flipcash_Community_V1_GetCommunityResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs._community != rhs._community {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Community_V1_GetCommunityResponse.Result: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}NOT_FOUND\0") +} diff --git a/Sources/Flipcash2ClientProtocol/community_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/community_v1_model.pb.swift new file mode 100644 index 0000000..ed60c9b --- /dev/null +++ b/Sources/Flipcash2ClientProtocol/community_v1_model.pb.swift @@ -0,0 +1,253 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: community/v1/model.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// CommunityMetadata is what Flipcash shows about the people and groups around +/// one currency launched on Flipcash. A community is identified by its +/// currency's mint, and there is exactly one per launched currency. +/// +/// More fields will be added as the community surface grows. +public struct Flipcash_Community_V1_CommunityMetadata: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The mint of the community's currency. + public var mint: Flipcash_Common_V1_PublicKey { + get {return _storage._mint ?? Flipcash_Common_V1_PublicKey()} + set {_uniqueStorage()._mint = newValue} + } + /// Returns true if `mint` has been explicitly set. + public var hasMint: Bool {return _storage._mint != nil} + /// Clears the value of `mint`. Subsequent reads from it will return its default value. + public mutating func clearMint() {_uniqueStorage()._mint = nil} + + /// The name of the community's currency, as it was launched. Bounded as + /// OCP bounds a currency's name. + public var name: String { + get {return _storage._name} + set {_uniqueStorage()._name = newValue} + } + + /// The community's leader: the Flipcash user who launched its currency. + /// Unset when the account that launched it does not belong to a Flipcash + /// user, or belongs to one who has not set a display name. + public var leader: Flipcash_Community_V1_CommunityLeader { + get {return _storage._leader ?? Flipcash_Community_V1_CommunityLeader()} + set {_uniqueStorage()._leader = newValue} + } + /// Returns true if `leader` has been explicitly set. + public var hasLeader: Bool {return _storage._leader != nil} + /// Clears the value of `leader`. Subsequent reads from it will return its default value. + public mutating func clearLeader() {_uniqueStorage()._leader = nil} + + /// The public groups gated on this currency whose members hold the most of + /// it, most first by chat.v1.HoldingsSummary.quarks. Empty when no group + /// qualifies or none has been ranked yet. + /// + /// A group qualifies when it is a public group gated on this currency + /// alone: its minimum balance requirements, listener and speaker alike, + /// all name this currency's mint and no other (see chat.v1.HoldingsSummary + /// for the full definition). Private groups never qualify. Every group + /// listed carries holdings_summary. + /// + /// The ranking is a periodic snapshot taken one group at a time, so the + /// groups' holdings may have been computed at different times (see + /// chat.v1.HoldingsSummary.as_of) and the order may trail recent balance + /// changes. How many groups are returned, how often they are refreshed, + /// and any minimum a group must meet to be listed (such as a number of + /// holders) are the server's to decide and may change. + /// + /// Each group is returned as a list view shows it: chat_id, type, title, + /// description, profile_picture, roster_summary, rules, creator, + /// last_activity and holdings_summary. Nothing about the viewer's place in + /// the group is set (members, is_hidden, viewer_state, in_lobby), nor its + /// messaging state (last_message, latest_event_sequence), nor its + /// cover_picture: a client opening the group fetches the rest with + /// chat.v1.Chat.GetChat. The group is read when the community is, so its + /// title and pictures are current even when its holdings are not. + public var topHolderChats: [Flipcash_Chat_V1_Metadata] { + get {return _storage._topHolderChats} + set {_uniqueStorage()._topHolderChats = newValue} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +/// CommunityLeader is the user who leads a community (see +/// CommunityMetadata.leader). More fields will be added as the community +/// surface grows. +public struct Flipcash_Community_V1_CommunityLeader: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The leader's public profile, as a chat member row shows it: user ID, + /// display name (never empty), username, profile picture and Flipcard + /// customization. + public var profile: Flipcash_Profile_V1_UserProfile { + get {return _profile ?? Flipcash_Profile_V1_UserProfile()} + set {_profile = newValue} + } + /// Returns true if `profile` has been explicitly set. + public var hasProfile: Bool {return self._profile != nil} + /// Clears the value of `profile`. Subsequent reads from it will return its default value. + public mutating func clearProfile() {self._profile = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _profile: Flipcash_Profile_V1_UserProfile? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "flipcash.community.v1" + +extension Flipcash_Community_V1_CommunityMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CommunityMetadata" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}mint\0\u{1}name\0\u{1}leader\0\u{3}top_holder_chats\0") + + fileprivate class _StorageClass { + var _mint: Flipcash_Common_V1_PublicKey? = nil + var _name: String = String() + var _leader: Flipcash_Community_V1_CommunityLeader? = nil + var _topHolderChats: [Flipcash_Chat_V1_Metadata] = [] + + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _mint = source._mint + _name = source._name + _leader = source._leader + _topHolderChats = source._topHolderChats + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + public mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._mint) }() + case 2: try { try decoder.decodeSingularStringField(value: &_storage._name) }() + case 3: try { try decoder.decodeSingularMessageField(value: &_storage._leader) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &_storage._topHolderChats) }() + default: break + } + } + } + } + + public func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._mint { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !_storage._name.isEmpty { + try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 2) + } + try { if let v = _storage._leader { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if !_storage._topHolderChats.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._topHolderChats, fieldNumber: 4) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Community_V1_CommunityMetadata, rhs: Flipcash_Community_V1_CommunityMetadata) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._mint != rhs_storage._mint {return false} + if _storage._name != rhs_storage._name {return false} + if _storage._leader != rhs_storage._leader {return false} + if _storage._topHolderChats != rhs_storage._topHolderChats {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Community_V1_CommunityLeader: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CommunityLeader" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}profile\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._profile) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._profile { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Community_V1_CommunityLeader, rhs: Flipcash_Community_V1_CommunityLeader) -> Bool { + if lhs._profile != rhs._profile {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/Flipcash2ClientProtocol/intent_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/intent_v1_model.pb.swift index 4ea61b1..7fb3adc 100644 --- a/Sources/Flipcash2ClientProtocol/intent_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/intent_v1_model.pb.swift @@ -127,12 +127,21 @@ public struct Flipcash_Intent_V1_ChatMetadata: Sendable { set {type = .groupPayment(newValue)} } + public var reactionBoost: Flipcash_Intent_V1_ChatMetadata.ReactionBoost { + get { + if case .reactionBoost(let v)? = type {return v} + return Flipcash_Intent_V1_ChatMetadata.ReactionBoost() + } + set {type = .reactionBoost(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Type: Equatable, Sendable { case contactDmPayment(Flipcash_Intent_V1_ChatMetadata.ContactDmPayment) case dmPayment(Flipcash_Intent_V1_ChatMetadata.DmPayment) case groupPayment(Flipcash_Intent_V1_ChatMetadata.GroupPayment) + case reactionBoost(Flipcash_Intent_V1_ChatMetadata.ReactionBoost) } @@ -243,6 +252,77 @@ public struct Flipcash_Intent_V1_ChatMetadata: Sendable { public init() {} } + /// For paying to boost a reaction on a message in a group chat: the + /// payer's reaction with the emoji carries money, shown as + /// messaging.v1.Reactor.boost_usd and summed into + /// messaging.v1.EmojiReaction.boost_usd, which orders a message's + /// reactions. The chat_id must be a group the payer is a member + /// of, the message one of its reactable messages, and the payer must + /// already have reacted to it with the emoji: a boost puts money behind + /// a reaction the payer holds, so a client adds the reaction first + /// (messaging.v1.AddReaction) and pays after. No one boosts their own + /// message, the group's creator included. The payment is divided + /// between the message's sender and the group's creator by a fixed rule + /// the server enforces; the payer does not choose it: + /// + /// - A boost by anyone but the creator of a message the creator did + /// not send pays the creator 20% of the whole and the sender the + /// rest, as one SendPublicMultiPayment to the two of them. The + /// split is judged on each payment's fiat amount (its exchange data's + /// native_amount), which must be within half the currency's smallest + /// unit of the payee's share of the whole, so round each share to that + /// unit (e.g. a cent) either way. Each payment's quarks are the ones + /// worth its own amount, computed from that amount alone at the shared + /// exchange rate and reserve state; on a bonding curve they are not + /// an 80/20 of the quarks for the whole. + /// - A boost of the creator's own message pays the creator the whole. + /// - A boost by the creator pays the sender the whole. + /// + /// A boost that pays one person is a SendPublicPayment, and one that pays + /// two a SendPublicMultiPayment; the server refuses either intent for the + /// other case. Every share, each destination's amount, must be at least + /// the currency's minimum transfer value (one minor unit, e.g. $0.01), so + /// a boost that pays two is at least five of them. Either way the reaction + /// is credited with the whole payment. + /// + /// Once the payment is made the server records the + /// boost against the reaction: the emoji's and the payer's totals grow + /// by the payment's USD market value (the reaction is added back if the + /// payer removed it after paying), and a messaging.v1.ReactionUpdate + /// with action BOOSTED is broadcast. No message is posted into the chat. + public struct ReactionBoost: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The message whose reaction is boosted. + public var messageID: Flipcash_Messaging_V1_MessageId { + get {return _messageID ?? Flipcash_Messaging_V1_MessageId()} + set {_messageID = newValue} + } + /// Returns true if `messageID` has been explicitly set. + public var hasMessageID: Bool {return self._messageID != nil} + /// Clears the value of `messageID`. Subsequent reads from it will return its default value. + public mutating func clearMessageID() {self._messageID = nil} + + /// The emoji boosted. + public var emoji: Flipcash_Messaging_V1_Emoji { + get {return _emoji ?? Flipcash_Messaging_V1_Emoji()} + set {_emoji = newValue} + } + /// Returns true if `emoji` has been explicitly set. + public var hasEmoji: Bool {return self._emoji != nil} + /// Clears the value of `emoji`. Subsequent reads from it will return its default value. + public mutating func clearEmoji() {self._emoji = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _messageID: Flipcash_Messaging_V1_MessageId? = nil + fileprivate var _emoji: Flipcash_Messaging_V1_Emoji? = nil + } + public init() {} fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil @@ -304,7 +384,7 @@ extension Flipcash_Intent_V1_AppMetadata: SwiftProtobuf.Message, SwiftProtobuf._ extension Flipcash_Intent_V1_ChatMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ChatMetadata" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}contact_dm_payment\0\u{3}dm_payment\0\u{3}group_payment\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}contact_dm_payment\0\u{3}dm_payment\0\u{3}group_payment\0\u{3}reaction_boost\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -352,6 +432,19 @@ extension Flipcash_Intent_V1_ChatMetadata: SwiftProtobuf.Message, SwiftProtobuf. self.type = .groupPayment(v) } }() + case 5: try { + var v: Flipcash_Intent_V1_ChatMetadata.ReactionBoost? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .reactionBoost(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .reactionBoost(v) + } + }() default: break } } @@ -378,6 +471,10 @@ extension Flipcash_Intent_V1_ChatMetadata: SwiftProtobuf.Message, SwiftProtobuf. guard case .groupPayment(let v)? = self.type else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 4) }() + case .reactionBoost?: try { + guard case .reactionBoost(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -498,3 +595,42 @@ extension Flipcash_Intent_V1_ChatMetadata.GroupPayment: SwiftProtobuf.Message, S return true } } + +extension Flipcash_Intent_V1_ChatMetadata.ReactionBoost: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Intent_V1_ChatMetadata.protoMessageName + ".ReactionBoost" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}message_id\0\u{1}emoji\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._messageID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._emoji) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._messageID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._emoji { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Intent_V1_ChatMetadata.ReactionBoost, rhs: Flipcash_Intent_V1_ChatMetadata.ReactionBoost) -> Bool { + if lhs._messageID != rhs._messageID {return false} + if lhs._emoji != rhs._emoji {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift b/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift index d1cc2f5..aa7eb08 100644 --- a/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift @@ -992,6 +992,11 @@ public struct Flipcash_Messaging_V1_RemoveReactionResponse: Sendable { case ok // = 0 case denied // = 1 case messageNotFound // = 2 + + /// The caller has paid to boost this emoji on the message (see + /// Reactor.boost_usd), and a boosted reaction is permanent. Nothing + /// changed. + case cannotRemoveBoosted // = 3 case UNRECOGNIZED(Int) public init() { @@ -1003,6 +1008,7 @@ public struct Flipcash_Messaging_V1_RemoveReactionResponse: Sendable { case 0: self = .ok case 1: self = .denied case 2: self = .messageNotFound + case 3: self = .cannotRemoveBoosted default: self = .UNRECOGNIZED(rawValue) } } @@ -1012,6 +1018,7 @@ public struct Flipcash_Messaging_V1_RemoveReactionResponse: Sendable { case .ok: return 0 case .denied: return 1 case .messageNotFound: return 2 + case .cannotRemoveBoosted: return 3 case .UNRECOGNIZED(let i): return i } } @@ -1021,6 +1028,7 @@ public struct Flipcash_Messaging_V1_RemoveReactionResponse: Sendable { .ok, .denied, .messageNotFound, + .cannotRemoveBoosted, ] } @@ -1063,13 +1071,16 @@ public struct Flipcash_Messaging_V1_GetReactorsRequest: Sendable { /// Clears the value of `emoji`. Subsequent reads from it will return its default value. public mutating func clearEmoji() {self._emoji = nil} - /// Paging over the reactor list. The order is fixed: reaction order, newest - /// first — descending Reactor.version, never reacted_ts. Leave + /// Paging over the reactor list. The order is fixed: most boosted first + /// (descending Reactor.boost_usd), then reaction order, newest first + /// (descending Reactor.version), never reacted_ts. Leave /// options.paging_token unset on the first request; on every subsequent /// request, set it to the paging_token from the most recent response to /// advance through the list. The token is opaque and server-generated; do - /// not construct it. options.page_size above 100 is clamped to 100, the most - /// a response carries; options.order is ignored. + /// not construct it, and do not keep it: a token the server no longer + /// recognises (one issued under an earlier order, say) is answered with + /// the first page, not an error. options.page_size above 100 is clamped to + /// 100, the most a response carries; options.order is ignored. public var options: Flipcash_Common_V1_QueryOptions { get {return _options ?? Flipcash_Common_V1_QueryOptions()} set {_options = newValue} @@ -1106,9 +1117,9 @@ public struct Flipcash_Messaging_V1_GetReactorsResponse: Sendable { public var result: Flipcash_Messaging_V1_GetReactorsResponse.Result = .ok - /// A page of users who reacted with the requested emoji, newest first (see - /// Reactor.version). Empty when the message exists but has no reactors for - /// the emoji. + /// A page of users who reacted with the requested emoji, most boosted + /// first and then newest first (see Reactor.version). Empty when the + /// message exists but has no reactors for the emoji. public var reactors: [Flipcash_Messaging_V1_Reactor] = [] /// The server-generated cursor advanced past this page. The client MUST send @@ -1135,8 +1146,15 @@ public struct Flipcash_Messaging_V1_GetReactorsResponse: Sendable { /// keeping an open reactor list live applies ReactionUpdates for this /// (message, emoji) whose version exceeds this value; any it re-applies are /// idempotent (an ADDED actor already listed dedupes, a REMOVED actor already - /// absent is a no-op). 0 when the emoji has never been reacted with on the - /// message. Set when result is OK. + /// absent is a no-op, a BOOSTED actor already at the update's + /// actor_boost_usd stays put). The boosted head of the list may be + /// served from an index that trails a boost by a moment, so a boost that + /// landed just before the page can be absent from it, or show the actor + /// at an earlier total; no update repairs that (the boost's version is at + /// or below this value), and the reactor appears on the next read. A + /// client may apply a BOOSTED update's actor_boost_usd whatever its + /// version, since a total re-applied is the same total. 0 when the emoji + /// has never been reacted with on the message. Set when result is OK. public var version: UInt64 = 0 public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2355,7 +2373,7 @@ extension Flipcash_Messaging_V1_RemoveReactionResponse: SwiftProtobuf.Message, S } extension Flipcash_Messaging_V1_RemoveReactionResponse.Result: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}MESSAGE_NOT_FOUND\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}MESSAGE_NOT_FOUND\0\u{1}CANNOT_REMOVE_BOOSTED\0") } extension Flipcash_Messaging_V1_GetReactorsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { diff --git a/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift index c2a53b1..fa39242 100644 --- a/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift @@ -1166,13 +1166,25 @@ public struct Flipcash_Messaging_V1_Reactor: Sendable { /// The emoji aggregate's version at which this reaction was added — the /// EmojiReaction.version the add produced. Unique among an emoji's reactors, - /// so it is the reactor list's order: descending version is reaction order, - /// newest first, with no clock involved. A reactor who removes and re-adds - /// comes back under a fresh, higher version, at the top. A client slotting a - /// ReactionUpdate's actor into sample_reactors records the update's version - /// here. + /// so with boost_usd it makes the reactor list's order total: the list is + /// most boosted first (descending boost_usd), then reaction order, newest + /// first (descending version), with no clock involved. Every plain + /// reaction sits after every boosted one. A reactor who removes and re-adds + /// comes back under a fresh, higher version, at the top of the plain + /// reactors. A client slotting a ReactionUpdate's actor into + /// sample_reactors records the update's version here. public var version: UInt64 = 0 + /// The total the user has paid to boost this emoji on this message, in USD + /// at each payment's market value when it was made (see + /// intent.v1.ChatMetadata.ReactionBoost). 0 when they have only reacted. + /// A boosted reaction is permanent: it cannot be removed, and the total + /// only grows. It is the reactor list's primary order (see version): a + /// client applying a BOOSTED ReactionUpdate records the update's + /// actor_boost_usd here and moves the actor to their place among the + /// boosters. + public var boostUsd: Double = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1200,7 +1212,13 @@ public struct Flipcash_Messaging_V1_ReactionSummary: Sendable { /// Clears the value of `messageID`. Subsequent reads from it will return its default value. public mutating func clearMessageID() {self._messageID = nil} - /// One entry per distinct emoji reacted to this message + /// One entry per distinct emoji reacted to this message, in the server's + /// display order: most money first (descending EmojiReaction.boost_usd), + /// then most reactors (descending count), then by the emoji itself. The + /// order is total for a given state, so two reads of the same state list + /// the same sequence. Counts and totals move as users react and boost, so + /// a client that wants a row to hold still between refreshes keeps its own + /// order; this is the order for a fresh render. public var reactions: [Flipcash_Messaging_V1_EmojiReaction] = [] public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -1259,9 +1277,10 @@ public struct Flipcash_Messaging_V1_EmojiReaction: Sendable { public mutating func clearSelfReactor() {self._selfReactor = nil} /// A small sample of reactors (e.g. for rendering a few avatars), capped - /// well below count: the most recent reactors by Reactor.version, newest - /// first. The complete, paged reactor list is fetched on demand via - /// GetReactors, in the same order. + /// well below count: the first reactors in list order (see + /// Reactor.version), so the top boosters by Reactor.boost_usd and then the + /// most recent plain reactors, newest first. The complete, paged reactor + /// list is fetched on demand via GetReactors, in the same order. public var sampleReactors: [Flipcash_Messaging_V1_Reactor] = [] /// Version of this emoji's aggregate on the message: assigned by the server @@ -1287,6 +1306,18 @@ public struct Flipcash_Messaging_V1_EmojiReaction: Sendable { /// restore a watermark once forgotten. public var version: UInt64 = 0 + /// The total paid to boost this emoji on this message, in USD at each + /// payment's market value when it was made: the sum of every reactor's + /// boost_usd. 0 when no one has boosted it. It only grows, since a boosted + /// reaction cannot be removed, and it moves `version` like any other + /// change to the emoji. The server keeps every boost as its own record + /// behind these totals; there is no read of them yet. + /// + /// It is the primary sort key of a ReactionSummary (see there): the server + /// lists an emoji with more money behind it ahead of one with more + /// reactors. + public var boostUsd: Double = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1315,7 +1346,7 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { /// Clears the value of `messageID`. Subsequent reads from it will return its default value. public mutating func clearMessageID() {self._messageID = nil} - /// The emoji that was added or removed. + /// The emoji that was added, removed or boosted. public var emoji: Flipcash_Messaging_V1_Emoji { get {return _emoji ?? Flipcash_Messaging_V1_Emoji()} set {_emoji = newValue} @@ -1325,10 +1356,11 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { /// Clears the value of `emoji`. Subsequent reads from it will return its default value. public mutating func clearEmoji() {self._emoji = nil} - /// The user who added or removed the reaction. A client maintains its own - /// EmojiReaction.self_reactor by comparing this to itself (ADDED sets it - /// from this actor, `version` and `reacted_ts` below; REMOVED clears it), - /// so a reaction made on the user's other device is reflected. + /// The user who added, removed or boosted the reaction. A client maintains + /// its own EmojiReaction.self_reactor by comparing this to itself (ADDED + /// sets it from this actor, `version` and `reacted_ts` below; REMOVED + /// clears it; BOOSTED sets it if absent and records actor_boost_usd on + /// it), so a reaction made on the user's other device is reflected. public var actor: Flipcash_Common_V1_UserId { get {return _actor ?? Flipcash_Common_V1_UserId()} set {_actor = newValue} @@ -1355,8 +1387,12 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { /// When the actor reacted. On ADDED, clients record this as the actor's /// Reactor.reacted_ts, and `version` above as their Reactor.version, when /// slotting them into sample_reactors or an open reactor list; ignored for - /// REMOVED. This is a display timestamp, distinct from `version`, which is - /// the ordering key. + /// REMOVED. On BOOSTED it is when the actor first reacted with the emoji: + /// the boost's time when the boost added the reaction, the original add's + /// time otherwise, so a client slotting a new reactor records it as above, + /// and one already listed keeps its reacted_ts and version while + /// actor_boost_usd moves it among the boosters. This is a display + /// timestamp, distinct from `version`, which is the ordering key. public var reactedTs: SwiftProtobuf.Google_Protobuf_Timestamp { get {return _reactedTs ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_reactedTs = newValue} @@ -1366,6 +1402,15 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { /// Clears the value of `reactedTs`. Subsequent reads from it will return its default value. public mutating func clearReactedTs() {self._reactedTs = nil} + /// The emoji's total boost after this change (EmojiReaction.boost_usd). + /// Set on BOOSTED; on ADDED and REMOVED it is the unchanged total, carried + /// so a client applying the update at `version` holds the whole aggregate. + public var boostUsd: Double = 0 + + /// The actor's own total on the emoji after this change (their + /// Reactor.boost_usd). Set on BOOSTED; 0 on ADDED and REMOVED. + public var actorBoostUsd: Double = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum Action: SwiftProtobuf.Enum, Swift.CaseIterable { @@ -1373,6 +1418,15 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { case unknown // = 0 case added // = 1 case removed // = 2 + + /// The actor paid to boost the emoji (see + /// intent.v1.ChatMetadata.ReactionBoost). `count` is the reactor count + /// after it, one more than before when the actor had not reacted with + /// the emoji yet, unchanged otherwise; boost_usd and actor_boost_usd + /// carry the new totals. Applied by `version` like any other action, + /// so a client that missed the ADDED it implies still ends up with + /// the actor listed. + case boosted // = 3 case UNRECOGNIZED(Int) public init() { @@ -1384,6 +1438,7 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { case 0: self = .unknown case 1: self = .added case 2: self = .removed + case 3: self = .boosted default: self = .UNRECOGNIZED(rawValue) } } @@ -1393,6 +1448,7 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { case .unknown: return 0 case .added: return 1 case .removed: return 2 + case .boosted: return 3 case .UNRECOGNIZED(let i): return i } } @@ -1402,6 +1458,7 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { .unknown, .added, .removed, + .boosted, ] } @@ -2714,7 +2771,7 @@ extension Flipcash_Messaging_V1_Emoji: SwiftProtobuf.Message, SwiftProtobuf._Mes extension Flipcash_Messaging_V1_Reactor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Reactor" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}reacted_ts\0\u{1}version\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}reacted_ts\0\u{1}version\0\u{3}boost_usd\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2725,6 +2782,7 @@ extension Flipcash_Messaging_V1_Reactor: SwiftProtobuf.Message, SwiftProtobuf._M case 1: try { try decoder.decodeSingularMessageField(value: &self._userID) }() case 2: try { try decoder.decodeSingularMessageField(value: &self._reactedTs) }() case 3: try { try decoder.decodeSingularUInt64Field(value: &self.version) }() + case 4: try { try decoder.decodeSingularDoubleField(value: &self.boostUsd) }() default: break } } @@ -2744,6 +2802,9 @@ extension Flipcash_Messaging_V1_Reactor: SwiftProtobuf.Message, SwiftProtobuf._M if self.version != 0 { try visitor.visitSingularUInt64Field(value: self.version, fieldNumber: 3) } + if self.boostUsd.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.boostUsd, fieldNumber: 4) + } try unknownFields.traverse(visitor: &visitor) } @@ -2751,6 +2812,7 @@ extension Flipcash_Messaging_V1_Reactor: SwiftProtobuf.Message, SwiftProtobuf._M if lhs._userID != rhs._userID {return false} if lhs._reactedTs != rhs._reactedTs {return false} if lhs.version != rhs.version {return false} + if lhs.boostUsd != rhs.boostUsd {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2797,7 +2859,7 @@ extension Flipcash_Messaging_V1_ReactionSummary: SwiftProtobuf.Message, SwiftPro extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".EmojiReaction" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}emoji\0\u{1}count\0\u{3}self_reactor\0\u{3}sample_reactors\0\u{1}version\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}emoji\0\u{1}count\0\u{3}self_reactor\0\u{3}sample_reactors\0\u{1}version\0\u{3}boost_usd\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2810,6 +2872,7 @@ extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProto case 3: try { try decoder.decodeSingularMessageField(value: &self._selfReactor) }() case 4: try { try decoder.decodeRepeatedMessageField(value: &self.sampleReactors) }() case 5: try { try decoder.decodeSingularUInt64Field(value: &self.version) }() + case 6: try { try decoder.decodeSingularDoubleField(value: &self.boostUsd) }() default: break } } @@ -2835,6 +2898,9 @@ extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProto if self.version != 0 { try visitor.visitSingularUInt64Field(value: self.version, fieldNumber: 5) } + if self.boostUsd.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.boostUsd, fieldNumber: 6) + } try unknownFields.traverse(visitor: &visitor) } @@ -2844,6 +2910,7 @@ extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProto if lhs._selfReactor != rhs._selfReactor {return false} if lhs.sampleReactors != rhs.sampleReactors {return false} if lhs.version != rhs.version {return false} + if lhs.boostUsd != rhs.boostUsd {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2851,7 +2918,7 @@ extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProto extension Flipcash_Messaging_V1_ReactionUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ReactionUpdate" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}message_id\0\u{1}emoji\0\u{1}actor\0\u{1}action\0\u{1}count\0\u{1}version\0\u{3}reacted_ts\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}message_id\0\u{1}emoji\0\u{1}actor\0\u{1}action\0\u{1}count\0\u{1}version\0\u{3}reacted_ts\0\u{3}boost_usd\0\u{3}actor_boost_usd\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2866,6 +2933,8 @@ extension Flipcash_Messaging_V1_ReactionUpdate: SwiftProtobuf.Message, SwiftProt case 5: try { try decoder.decodeSingularUInt64Field(value: &self.count) }() case 6: try { try decoder.decodeSingularUInt64Field(value: &self.version) }() case 7: try { try decoder.decodeSingularMessageField(value: &self._reactedTs) }() + case 8: try { try decoder.decodeSingularDoubleField(value: &self.boostUsd) }() + case 9: try { try decoder.decodeSingularDoubleField(value: &self.actorBoostUsd) }() default: break } } @@ -2897,6 +2966,12 @@ extension Flipcash_Messaging_V1_ReactionUpdate: SwiftProtobuf.Message, SwiftProt try { if let v = self._reactedTs { try visitor.visitSingularMessageField(value: v, fieldNumber: 7) } }() + if self.boostUsd.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.boostUsd, fieldNumber: 8) + } + if self.actorBoostUsd.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.actorBoostUsd, fieldNumber: 9) + } try unknownFields.traverse(visitor: &visitor) } @@ -2908,13 +2983,15 @@ extension Flipcash_Messaging_V1_ReactionUpdate: SwiftProtobuf.Message, SwiftProt if lhs.count != rhs.count {return false} if lhs.version != rhs.version {return false} if lhs._reactedTs != rhs._reactedTs {return false} + if lhs.boostUsd != rhs.boostUsd {return false} + if lhs.actorBoostUsd != rhs.actorBoostUsd {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } extension Flipcash_Messaging_V1_ReactionUpdate.Action: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}ADDED\0\u{1}REMOVED\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}ADDED\0\u{1}REMOVED\0\u{1}BOOSTED\0") } extension Flipcash_Messaging_V1_ReactionUpdateBatch: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { diff --git a/flipcash2.lock b/flipcash2.lock index 4d81627..7bf54d7 100644 --- a/flipcash2.lock +++ b/flipcash2.lock @@ -1,4 +1,4 @@ # Pinned upstream contract. Regenerate with scripts/sync-protos.sh . upstream: code-payments/flipcash2-protobuf-api -commit: 1897108ae75e032fc5f7de13cf6e9cadcec9deda -subject: Update link preview comments (#146) +commit: 733ea7f9f33291b9631f0176fab5485acf161fab +subject: Update comments around paid reactions (#149) diff --git a/proto/activity/v1/model.proto b/proto/activity/v1/model.proto index 1b92ef1..26e3055 100644 --- a/proto/activity/v1/model.proto +++ b/proto/activity/v1/model.proto @@ -7,6 +7,7 @@ option java_package = "com.codeinc.flipcash.gen.activity.v1"; option objc_class_prefix = "FCPBActivityV1"; import "common/v1/common.proto"; +import "messaging/v1/model.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -64,6 +65,12 @@ message DirectlySentCryptoNotificationMetadata { common.v1.PhoneNumber phone = 1; common.v1.UserId user_id = 2; } + + // Set when the payment boosted a reaction (see BoostedReaction). A boost + // that paid one person names them in destination_identifier; one that + // paid both the message's sender and the group's creator names neither, + // and payment_amount is the whole boost. + BoostedReaction boosted_reaction = 3; } message ReceivedCryptoNotificationMetadata { @@ -71,6 +78,28 @@ message ReceivedCryptoNotificationMetadata { common.v1.PhoneNumber phone = 1; common.v1.UserId user_id = 2; } + + // Set when the payment was the recipient's share of a boost of a + // reaction (see BoostedReaction), paid to them as the boosted message's + // sender or the group's creator. source_identifier names the payer, and + // payment_amount is the recipient's share alone. + BoostedReaction boosted_reaction = 3; +} + +// The reaction a payment boosted: the payer's reaction with emoji on a +// message in a group chat (see intent.v1.ChatMetadata.ReactionBoost). It is +// what the payment was for when it was made and is not kept up to date: the +// message may since have been deleted, and the viewer may no longer be a +// member of the chat. +message BoostedReaction { + // The group chat the boosted message is in. + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + // The message whose reaction was boosted. + messaging.v1.MessageId message_id = 2 [(validate.rules).message.required = true]; + + // The emoji of the boosted reaction. + messaging.v1.Emoji emoji = 3 [(validate.rules).message.required = true]; } message WithdrewCryptoNotificationMetadata { diff --git a/proto/chat/v1/model.proto b/proto/chat/v1/model.proto index a721941..86e7945 100644 --- a/proto/chat/v1/model.proto +++ b/proto/chat/v1/model.proto @@ -145,6 +145,19 @@ message Metadata { // feed result clear a cover picture the client already holds. blob.v1.Media cover_picture = 17; + // What the group's members hold of the currency the group is gated on. + // Only supported for public group chats gated on a single currency + // launched on Flipcash (see HoldingsSummary for what that means). Unset + // for every other chat, for such a group until the server has first + // computed it, and whenever too few of its members hold the currency: + // the server sets a minimum number of holders (and a positive total) + // below which it shows no summary, and may change it. A group whose + // holders fall below the minimum loses its summary at the next refresh. + // + // A periodic snapshot (see HoldingsSummary.as_of), not live: no event + // announces a change, and a client refetches the chat to refresh it. + HoldingsSummary holdings_summary = 18; + // Whether messages in this chat are end-to-end encrypted (see // messaging.v1.EncryptedContent). Only supported for DMs (CONTACT_DM or DM); // always false for group chats. A private group is always end-to-end @@ -342,6 +355,36 @@ message RosterSummary { uint64 version = 2; } +// HoldingsSummary is what a group's members held of the currency the group is +// gated on, as of one moment (see Metadata.holdings_summary). Holdings are +// counted for the members at that moment, whether or not each still meets +// the group's minimum balance requirements. +// +// A group is gated on a single currency when its minimum balance +// requirements (see MinimumBalanceRequirement), listener and speaker alike, +// all name the same one mint, and that mint is a currency launched on +// Flipcash. The fiat currency a requirement is denominated in does not +// matter. A group with no minimum balance requirement is not gated on a +// currency, nor is one with a requirement that names no mint (and so applies +// to every mint), nor one whose requirements name different mints. +message HoldingsSummary { + // The mint of the currency the group is gated on: the one mint its + // minimum balance requirements name. + common.v1.PublicKey mint = 1 [(validate.rules).message.required = true]; + + // The total amount of the currency held by the group's members, in the + // mint's quarks. Clients value it in fiat with the currency's price they + // already show. + uint64 quarks = 2; + + // The number of the group's members who held any of the currency. + uint64 holder_count = 3; + + // When quarks and holder_count were computed. How often the server + // recomputes them is its to decide and may change. + google.protobuf.Timestamp as_of = 4 [(validate.rules).timestamp.required = true]; +} + message MetadataUpdate { oneof kind { option (validate.required) = true; diff --git a/proto/community/v1/community_service.proto b/proto/community/v1/community_service.proto new file mode 100644 index 0000000..2ca3ef9 --- /dev/null +++ b/proto/community/v1/community_service.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +package flipcash.community.v1; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/community/v1;communitypb"; +option java_package = "com.codeinc.flipcash.gen.community.v1"; +option objc_class_prefix = "FPBCommunityV1"; + +import "common/v1/common.proto"; +import "community/v1/model.proto"; +import "validate/validate.proto"; + +// Community serves what Flipcash shows about the community around a currency +// launched on Flipcash (see CommunityMetadata). +// +// Everything it returns is public and the same for every viewer. +service Community { + // GetCommunity returns a currency's community. + // + // Auth is optional; when set it must be valid, but it changes nothing + // about what is returned. + rpc GetCommunity(GetCommunityRequest) returns (GetCommunityResponse); +} + +message GetCommunityRequest { + // The community to fetch. Only a mint is supported for now; the oneof + // leaves room for other identifiers. + oneof identifier { + option (validate.required) = true; + + // The mint of the community's currency. + common.v1.PublicKey mint = 1; + } + + // Optional. When set it must be valid, but the community is the same with + // or without it (see Community.GetCommunity). + common.v1.Auth auth = 10; +} + +message GetCommunityResponse { + Result result = 1; + enum Result { + OK = 0; + // The mint is not a currency launched on Flipcash, or is not yet + // available. + NOT_FOUND = 1; + } + + // Set only when result == OK. + CommunityMetadata community = 2; +} diff --git a/proto/community/v1/model.proto b/proto/community/v1/model.proto new file mode 100644 index 0000000..1f9720b --- /dev/null +++ b/proto/community/v1/model.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +package flipcash.community.v1; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/community/v1;communitypb"; +option java_package = "com.codeinc.flipcash.gen.community.v1"; +option objc_class_prefix = "FPBCommunityV1"; + +import "chat/v1/model.proto"; +import "common/v1/common.proto"; +import "profile/v1/model.proto"; +import "validate/validate.proto"; + +// CommunityMetadata is what Flipcash shows about the people and groups around +// one currency launched on Flipcash. A community is identified by its +// currency's mint, and there is exactly one per launched currency. +// +// More fields will be added as the community surface grows. +message CommunityMetadata { + // The mint of the community's currency. + common.v1.PublicKey mint = 1 [(validate.rules).message.required = true]; + + // The name of the community's currency, as it was launched. Bounded as + // OCP bounds a currency's name. + string name = 2 [(validate.rules).string = { + min_len: 1 + max_len: 32 + }]; + + // The community's leader: the Flipcash user who launched its currency. + // Unset when the account that launched it does not belong to a Flipcash + // user, or belongs to one who has not set a display name. + CommunityLeader leader = 3; + + // The public groups gated on this currency whose members hold the most of + // it, most first by chat.v1.HoldingsSummary.quarks. Empty when no group + // qualifies or none has been ranked yet. + // + // A group qualifies when it is a public group gated on this currency + // alone: its minimum balance requirements, listener and speaker alike, + // all name this currency's mint and no other (see chat.v1.HoldingsSummary + // for the full definition). Private groups never qualify. Every group + // listed carries holdings_summary. + // + // The ranking is a periodic snapshot taken one group at a time, so the + // groups' holdings may have been computed at different times (see + // chat.v1.HoldingsSummary.as_of) and the order may trail recent balance + // changes. How many groups are returned, how often they are refreshed, + // and any minimum a group must meet to be listed (such as a number of + // holders) are the server's to decide and may change. + // + // Each group is returned as a list view shows it: chat_id, type, title, + // description, profile_picture, roster_summary, rules, creator, + // last_activity and holdings_summary. Nothing about the viewer's place in + // the group is set (members, is_hidden, viewer_state, in_lobby), nor its + // messaging state (last_message, latest_event_sequence), nor its + // cover_picture: a client opening the group fetches the rest with + // chat.v1.Chat.GetChat. The group is read when the community is, so its + // title and pictures are current even when its holdings are not. + repeated chat.v1.Metadata top_holder_chats = 4 [(validate.rules).repeated = { + min_items: 0 + max_items: 25 // todo: arbitrary + }]; +} + +// CommunityLeader is the user who leads a community (see +// CommunityMetadata.leader). More fields will be added as the community +// surface grows. +message CommunityLeader { + // The leader's public profile, as a chat member row shows it: user ID, + // display name (never empty), username, profile picture and Flipcard + // customization. + profile.v1.UserProfile profile = 1 [(validate.rules).message.required = true]; +} diff --git a/proto/intent/v1/model.proto b/proto/intent/v1/model.proto index 7a84261..36d1338 100644 --- a/proto/intent/v1/model.proto +++ b/proto/intent/v1/model.proto @@ -7,6 +7,7 @@ option java_package = "com.codeinc.flipcash.gen.intent.v1"; option objc_class_prefix = "FPBIntentV1"; import "common/v1/common.proto"; +import "messaging/v1/model.proto"; import "validate/validate.proto"; message AppMetadata { @@ -27,6 +28,7 @@ message ChatMetadata { ContactDmPayment contact_dm_payment = 2; DmPayment dm_payment = 3; GroupPayment group_payment = 4; + ReactionBoost reaction_boost = 5; } // For sending a payment to a contact in a DM @@ -64,6 +66,52 @@ message ChatMetadata { // denies TIP. PaymentAction action = 1; } + + // For paying to boost a reaction on a message in a group chat: the + // payer's reaction with the emoji carries money, shown as + // messaging.v1.Reactor.boost_usd and summed into + // messaging.v1.EmojiReaction.boost_usd, which orders a message's + // reactions. The chat_id must be a group the payer is a member + // of, the message one of its reactable messages, and the payer must + // already have reacted to it with the emoji: a boost puts money behind + // a reaction the payer holds, so a client adds the reaction first + // (messaging.v1.AddReaction) and pays after. No one boosts their own + // message, the group's creator included. The payment is divided + // between the message's sender and the group's creator by a fixed rule + // the server enforces; the payer does not choose it: + // + // - A boost by anyone but the creator of a message the creator did + // not send pays the creator 20% of the whole and the sender the + // rest, as one SendPublicMultiPayment to the two of them. The + // split is judged on each payment's fiat amount (its exchange data's + // native_amount), which must be within half the currency's smallest + // unit of the payee's share of the whole, so round each share to that + // unit (e.g. a cent) either way. Each payment's quarks are the ones + // worth its own amount, computed from that amount alone at the shared + // exchange rate and reserve state; on a bonding curve they are not + // an 80/20 of the quarks for the whole. + // - A boost of the creator's own message pays the creator the whole. + // - A boost by the creator pays the sender the whole. + // + // A boost that pays one person is a SendPublicPayment, and one that pays + // two a SendPublicMultiPayment; the server refuses either intent for the + // other case. Every share, each destination's amount, must be at least + // the currency's minimum transfer value (one minor unit, e.g. $0.01), so + // a boost that pays two is at least five of them. Either way the reaction + // is credited with the whole payment. + // + // Once the payment is made the server records the + // boost against the reaction: the emoji's and the payer's totals grow + // by the payment's USD market value (the reaction is added back if the + // payer removed it after paying), and a messaging.v1.ReactionUpdate + // with action BOOSTED is broadcast. No message is posted into the chat. + message ReactionBoost { + // The message whose reaction is boosted. + messaging.v1.MessageId message_id = 1 [(validate.rules).message.required = true]; + + // The emoji boosted. + messaging.v1.Emoji emoji = 2 [(validate.rules).message.required = true]; + } } // The action being performed for a payment in a chat diff --git a/proto/messaging/v1/messaging_service.proto b/proto/messaging/v1/messaging_service.proto index 8235f53..9734ddb 100644 --- a/proto/messaging/v1/messaging_service.proto +++ b/proto/messaging/v1/messaging_service.proto @@ -403,9 +403,13 @@ message RemoveReactionRequest { message RemoveReactionResponse { Result result = 1; enum Result { - OK = 0; - DENIED = 1; - MESSAGE_NOT_FOUND = 2; + OK = 0; + DENIED = 1; + MESSAGE_NOT_FOUND = 2; + // The caller has paid to boost this emoji on the message (see + // Reactor.boost_usd), and a boosted reaction is permanent. Nothing + // changed. + CANNOT_REMOVE_BOOSTED = 3; } // The affected emoji's aggregate after the removal @@ -420,13 +424,16 @@ message GetReactorsRequest { // The emoji whose reactors to list. Emoji emoji = 3 [(validate.rules).message.required = true]; - // Paging over the reactor list. The order is fixed: reaction order, newest - // first — descending Reactor.version, never reacted_ts. Leave + // Paging over the reactor list. The order is fixed: most boosted first + // (descending Reactor.boost_usd), then reaction order, newest first + // (descending Reactor.version), never reacted_ts. Leave // options.paging_token unset on the first request; on every subsequent // request, set it to the paging_token from the most recent response to // advance through the list. The token is opaque and server-generated; do - // not construct it. options.page_size above 100 is clamped to 100, the most - // a response carries; options.order is ignored. + // not construct it, and do not keep it: a token the server no longer + // recognises (one issued under an earlier order, say) is answered with + // the first page, not an error. options.page_size above 100 is clamped to + // 100, the most a response carries; options.order is ignored. common.v1.QueryOptions options = 4; common.v1.Auth auth = 10 [(validate.rules).message.required = true]; @@ -440,9 +447,9 @@ message GetReactorsResponse { MESSAGE_NOT_FOUND = 2; } - // A page of users who reacted with the requested emoji, newest first (see - // Reactor.version). Empty when the message exists but has no reactors for - // the emoji. + // A page of users who reacted with the requested emoji, most boosted + // first and then newest first (see Reactor.version). Empty when the + // message exists but has no reactors for the emoji. repeated Reactor reactors = 2 [(validate.rules).repeated = { max_items: 100 }]; @@ -464,8 +471,15 @@ message GetReactorsResponse { // keeping an open reactor list live applies ReactionUpdates for this // (message, emoji) whose version exceeds this value; any it re-applies are // idempotent (an ADDED actor already listed dedupes, a REMOVED actor already - // absent is a no-op). 0 when the emoji has never been reacted with on the - // message. Set when result is OK. + // absent is a no-op, a BOOSTED actor already at the update's + // actor_boost_usd stays put). The boosted head of the list may be + // served from an index that trails a boost by a moment, so a boost that + // landed just before the page can be absent from it, or show the actor + // at an earlier total; no update repairs that (the boost's version is at + // or below this value), and the reactor appears on the next read. A + // client may apply a BOOSTED update's actor_boost_usd whatever its + // version, since a total re-applied is the same total. 0 when the emoji + // has never been reacted with on the message. Set when result is OK. uint64 version = 5; } diff --git a/proto/messaging/v1/model.proto b/proto/messaging/v1/model.proto index b052a5b..8851e31 100644 --- a/proto/messaging/v1/model.proto +++ b/proto/messaging/v1/model.proto @@ -670,12 +670,24 @@ message Reactor { // The emoji aggregate's version at which this reaction was added — the // EmojiReaction.version the add produced. Unique among an emoji's reactors, - // so it is the reactor list's order: descending version is reaction order, - // newest first, with no clock involved. A reactor who removes and re-adds - // comes back under a fresh, higher version, at the top. A client slotting a - // ReactionUpdate's actor into sample_reactors records the update's version - // here. + // so with boost_usd it makes the reactor list's order total: the list is + // most boosted first (descending boost_usd), then reaction order, newest + // first (descending version), with no clock involved. Every plain + // reaction sits after every boosted one. A reactor who removes and re-adds + // comes back under a fresh, higher version, at the top of the plain + // reactors. A client slotting a ReactionUpdate's actor into + // sample_reactors records the update's version here. uint64 version = 3 [(validate.rules).uint64.gte = 1]; + + // The total the user has paid to boost this emoji on this message, in USD + // at each payment's market value when it was made (see + // intent.v1.ChatMetadata.ReactionBoost). 0 when they have only reacted. + // A boosted reaction is permanent: it cannot be removed, and the total + // only grows. It is the reactor list's primary order (see version): a + // client applying a BOOSTED ReactionUpdate records the update's + // actor_boost_usd here and moves the actor to their place among the + // boosters. + double boost_usd = 4 [(validate.rules).double.gte = 0]; } // ReactionSummary is the aggregate reaction state attached to a message. It is @@ -686,7 +698,13 @@ message ReactionSummary { // The message these reactions belong to. MessageId message_id = 1 [(validate.rules).message.required = true]; - // One entry per distinct emoji reacted to this message + // One entry per distinct emoji reacted to this message, in the server's + // display order: most money first (descending EmojiReaction.boost_usd), + // then most reactors (descending count), then by the emoji itself. The + // order is total for a given state, so two reads of the same state list + // the same sequence. Counts and totals move as users react and boost, so + // a client that wants a row to hold still between refreshes keeps its own + // order; this is the order for a fresh render. repeated EmojiReaction reactions = 2; } @@ -721,9 +739,10 @@ message EmojiReaction { Reactor self_reactor = 3; // A small sample of reactors (e.g. for rendering a few avatars), capped - // well below count: the most recent reactors by Reactor.version, newest - // first. The complete, paged reactor list is fetched on demand via - // GetReactors, in the same order. + // well below count: the first reactors in list order (see + // Reactor.version), so the top boosters by Reactor.boost_usd and then the + // most recent plain reactors, newest first. The complete, paged reactor + // list is fetched on demand via GetReactors, in the same order. repeated Reactor sample_reactors = 4 [(validate.rules).repeated = { max_items: 8 // Sample only; not the full list }]; @@ -750,6 +769,18 @@ message EmojiReaction { // emoji entirely — their versions are not surfaced — so a refresh cannot // restore a watermark once forgotten. uint64 version = 5 [(validate.rules).uint64.gte = 1]; + + // The total paid to boost this emoji on this message, in USD at each + // payment's market value when it was made: the sum of every reactor's + // boost_usd. 0 when no one has boosted it. It only grows, since a boosted + // reaction cannot be removed, and it moves `version` like any other + // change to the emoji. The server keeps every boost as its own record + // behind these totals; there is no read of them yet. + // + // It is the primary sort key of a ReactionSummary (see there): the server + // lists an emoji with more money behind it ahead of one with more + // reactors. + double boost_usd = 6 [(validate.rules).double.gte = 0]; } // ReactionUpdate is a best-effort, real-time reaction change for a single @@ -761,13 +792,14 @@ message ReactionUpdate { // The message whose reactions changed. MessageId message_id = 1 [(validate.rules).message.required = true]; - // The emoji that was added or removed. + // The emoji that was added, removed or boosted. Emoji emoji = 2 [(validate.rules).message.required = true]; - // The user who added or removed the reaction. A client maintains its own - // EmojiReaction.self_reactor by comparing this to itself (ADDED sets it - // from this actor, `version` and `reacted_ts` below; REMOVED clears it), - // so a reaction made on the user's other device is reflected. + // The user who added, removed or boosted the reaction. A client maintains + // its own EmojiReaction.self_reactor by comparing this to itself (ADDED + // sets it from this actor, `version` and `reacted_ts` below; REMOVED + // clears it; BOOSTED sets it if absent and records actor_boost_usd on + // it), so a reaction made on the user's other device is reflected. common.v1.UserId actor = 3 [(validate.rules).message.required = true]; Action action = 4 [(validate.rules).enum = { @@ -777,6 +809,14 @@ message ReactionUpdate { UNKNOWN = 0; ADDED = 1; REMOVED = 2; + // The actor paid to boost the emoji (see + // intent.v1.ChatMetadata.ReactionBoost). `count` is the reactor count + // after it, one more than before when the actor had not reacted with + // the emoji yet, unchanged otherwise; boost_usd and actor_boost_usd + // carry the new totals. Applied by `version` like any other action, + // so a client that missed the ADDED it implies still ends up with + // the actor listed. + BOOSTED = 3; } // The emoji's total reactor count after this change. 0 means no reactors @@ -794,9 +834,22 @@ message ReactionUpdate { // When the actor reacted. On ADDED, clients record this as the actor's // Reactor.reacted_ts, and `version` above as their Reactor.version, when // slotting them into sample_reactors or an open reactor list; ignored for - // REMOVED. This is a display timestamp, distinct from `version`, which is - // the ordering key. + // REMOVED. On BOOSTED it is when the actor first reacted with the emoji: + // the boost's time when the boost added the reaction, the original add's + // time otherwise, so a client slotting a new reactor records it as above, + // and one already listed keeps its reacted_ts and version while + // actor_boost_usd moves it among the boosters. This is a display + // timestamp, distinct from `version`, which is the ordering key. google.protobuf.Timestamp reacted_ts = 7 [(validate.rules).timestamp.required = true]; + + // The emoji's total boost after this change (EmojiReaction.boost_usd). + // Set on BOOSTED; on ADDED and REMOVED it is the unchanged total, carried + // so a client applying the update at `version` holds the whole aggregate. + double boost_usd = 8 [(validate.rules).double.gte = 0]; + + // The actor's own total on the emoji after this change (their + // Reactor.boost_usd). Set on BOOSTED; 0 on ADDED and REMOVED. + double actor_boost_usd = 9 [(validate.rules).double.gte = 0]; } message ReactionUpdateBatch { From 4b5c3f38b7c31592c8adc7d3e0b05b1494436207 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 16 Oct 2026 17:18:08 -0400 Subject: [PATCH 2/2] docs(changelog): add 0.20.0 entry --- CHANGELOG.md | 48 +++++++++++++++++++ .../ContractInfo.swift | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17bb9de..c2ce07f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,54 @@ called out explicitly even when nothing else did. release notes, so a version with no entry here does not release. Write the entry in the same PR that syncs the contract, while the diff is still in front of you. +## 0.20.0 + +Synced to [`flipcash2-protobuf-api@733ea7f9`](https://github.com/code-payments/flipcash2-protobuf-api/commit/733ea7f9f33291b9631f0176fab5485acf161fab), +picking up [#147](https://github.com/code-payments/flipcash2-protobuf-api/pull/147), +[#148](https://github.com/code-payments/flipcash2-protobuf-api/pull/148) and +[#149](https://github.com/code-payments/flipcash2-protobuf-api/pull/149) (docs only). `community.v1` +is new. `activity.v1`, `chat.v1`, `intent.v1` and `messaging.v1` moved. + +This release adds the Community service, paid reaction boosts and a holdings summary on gated group +chats. Everything is additive. Two enums gained a case, appended at the end, so no existing value +moved. An exhaustive `switch` or `when` over either enum needs the new case before it compiles. + +### Added + +- `community.v1.Community`, a new service with one RPC, `GetCommunity`. It looks a community up by + `mint` and returns `CommunityMetadata` (`mint`, `name`, `leader`, and up to 25 + `top_holder_chats`), or `NOT_FOUND`. +- Reaction boosts. A payer puts money behind a reaction they already hold on a group message: + - `intent.v1.ChatMetadata.reaction_boost` (oneof field 5), a `ReactionBoost` naming the + `message_id` and `emoji`. + - A boost that pays two people, the sender and the group's creator split 80/20, is an ocp + `SendPublicMultiPayment`, so it needs `ocp-client-protocol` 0.8.0 or later. A boost that pays + one person is a `SendPublicPayment`. + - `messaging.v1.Reactor.boost_usd` (field 4), `EmojiReaction.boost_usd` (field 6), + `ReactionUpdate.boost_usd` (field 8) and `ReactionUpdate.actor_boost_usd` (field 9). + - `ReactionUpdate.Action.BOOSTED = 3`. + - `RemoveReactionResponse.Result.CANNOT_REMOVE_BOOSTED = 3`. A boosted reaction is permanent. + - `activity.v1.BoostedReaction`, set as `boosted_reaction` (field 3) on both + `DirectlySentCryptoNotificationMetadata` and `ReceivedCryptoNotificationMetadata` when the + payment was a boost. +- `chat.v1.Metadata.holdings_summary` (field 18), a `HoldingsSummary` of the group members' holdings + of the currency the group is gated on (`mint`, `quarks`, `holder_count`, `as_of`). It is set only + for public groups gated on a single Flipcash-launched currency with enough holders. It is a + periodic snapshot, and no event announces a change. + +### Changed + +- Reactor lists, `sample_reactors` and `ReactionSummary.reactions` now sort boosted entries first, + by `boost_usd` descending, then fall back to the previous order. Nothing changed on the wire, but + a client that re-sorts by `version` alone will disagree with the server. +- `GetReactors` now documents that a paging token the server no longer recognises, such as one + issued under the old order, is answered with the first page rather than an error. Don't keep + tokens across sessions. + +### Unchanged + +Nothing was renumbered. No service, RPC, message, field or enum case was removed or changed type. + ## 0.19.0 Synced to [`flipcash2-protobuf-api@1897108a`](https://github.com/code-payments/flipcash2-protobuf-api/commit/1897108ae75e032fc5f7de13cf6e9cadcec9deda), diff --git a/Sources/Flipcash2ClientProtocol/ContractInfo.swift b/Sources/Flipcash2ClientProtocol/ContractInfo.swift index ac8f667..a6342af 100644 --- a/Sources/Flipcash2ClientProtocol/ContractInfo.swift +++ b/Sources/Flipcash2ClientProtocol/ContractInfo.swift @@ -1,7 +1,7 @@ // Generated by scripts/generate-swift.sh. Do not edit. public enum Flipcash2ContractInfo { - public static let version = "0.19.0-dev" + public static let version = "0.20.0-dev" public static let protoCommit = "733ea7f9f33291b9631f0176fab5485acf161fab" public static var isLocal: Bool { protoCommit == localSentinel }