From f4d7f77ccd5088c589c058467075126bc42de019 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 18 Sep 2026 16:01:30 -0400 Subject: [PATCH 1/3] chore(contract): sync flipcash2 protos to dd5e92db --- .../ContractInfo.swift | 2 +- .../blob_v1_model.pb.swift | 7 + .../chat_v1_chat_service.grpc.swift | 306 +++++++++++++- .../chat_v1_chat_service.pb.swift | 387 +++++++++++++++++- .../chat_v1_model.pb.swift | 376 ++++++++++++++++- ...vent_v1_event_streaming_service.grpc.swift | 76 +++- .../event_v1_event_streaming_service.pb.swift | 169 +++++++- .../event_v1_model.pb.swift | 2 +- .../messaging_v1_messaging_service.grpc.swift | 68 ++- .../messaging_v1_messaging_service.pb.swift | 89 +++- .../messaging_v1_model.pb.swift | 226 ++++++++-- .../push_v1_model.pb.swift | 12 +- .../reporting_v1_reporting_service.grpc.swift | 221 ++++++++++ .../reporting_v1_reporting_service.pb.swift | 369 +++++++++++++++++ flipcash2.lock | 4 +- proto/blob/v1/model.proto | 9 +- proto/chat/v1/chat_service.proto | 70 ++++ proto/chat/v1/model.proto | 54 ++- proto/event/v1/event_streaming_service.proto | 93 ++++- proto/event/v1/model.proto | 2 +- proto/messaging/v1/messaging_service.proto | 84 +++- proto/messaging/v1/model.proto | 155 ++++++- proto/push/v1/model.proto | 5 + proto/reporting/v1/reporting_service.proto | 65 +++ 24 files changed, 2743 insertions(+), 108 deletions(-) create mode 100644 Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.grpc.swift create mode 100644 Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.pb.swift create mode 100644 proto/reporting/v1/reporting_service.proto diff --git a/Sources/Flipcash2ClientProtocol/ContractInfo.swift b/Sources/Flipcash2ClientProtocol/ContractInfo.swift index f3a8520..dce6762 100644 --- a/Sources/Flipcash2ClientProtocol/ContractInfo.swift +++ b/Sources/Flipcash2ClientProtocol/ContractInfo.swift @@ -2,7 +2,7 @@ public enum Flipcash2ContractInfo { public static let version = "0.9.0-dev" - public static let protoCommit = "e1f4116c499718401d002cf79ce64229149ee297" + public static let protoCommit = "dd5e92db6f76700ab6b565f4eff1f9fb25140c9a" public static var isLocal: Bool { protoCommit == localSentinel } diff --git a/Sources/Flipcash2ClientProtocol/blob_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/blob_v1_model.pb.swift index bedb991..5ea1904 100644 --- a/Sources/Flipcash2ClientProtocol/blob_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/blob_v1_model.pb.swift @@ -255,6 +255,13 @@ public struct Flipcash_Blob_V1_BlobMetadata: Sendable { /// Ephemeral, server-minted URL for fetching the blob bytes, together with /// the instant it expires. Re-issued on every fetch — see DownloadUrl. + /// + /// Absent when the viewer may know what the blob is but may not fetch its + /// bytes: a redacted message (see messaging.v1.Message.redacted) carries + /// each rendition's intrinsic metadata — for an image, its dimensions and + /// blurhash — without a URL. A client renders such media from the blurhash + /// alone and does not call GetBlobs to mint one; the blob is not readable + /// by that viewer and the call would not return it. public var downloadURL: Flipcash_Blob_V1_DownloadUrl { get {return _downloadURL ?? Flipcash_Blob_V1_DownloadUrl()} set {_downloadURL = newValue} diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift index 399614f..50f6a18 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift @@ -92,6 +92,30 @@ public enum Flipcash_Chat_V1_Chat { method: "LeaveChat" ) } + /// Namespace for "MuteChat" metadata. + public enum MuteChat { + /// Request type for "MuteChat". + public typealias Input = Flipcash_Chat_V1_MuteChatRequest + /// Response type for "MuteChat". + public typealias Output = Flipcash_Chat_V1_MuteChatResponse + /// Descriptor for "MuteChat". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.chat.v1.Chat"), + method: "MuteChat" + ) + } + /// Namespace for "UnmuteChat" metadata. + public enum UnmuteChat { + /// Request type for "UnmuteChat". + public typealias Input = Flipcash_Chat_V1_UnmuteChatRequest + /// Response type for "UnmuteChat". + public typealias Output = Flipcash_Chat_V1_UnmuteChatResponse + /// Descriptor for "UnmuteChat". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.chat.v1.Chat"), + method: "UnmuteChat" + ) + } /// Descriptors for all methods in the "flipcash.chat.v1.Chat" service. public static let descriptors: [GRPCCore.MethodDescriptor] = [ GetChat.descriptor, @@ -99,7 +123,9 @@ public enum Flipcash_Chat_V1_Chat { GetGroupChatFeed.descriptor, StartChat.descriptor, JoinChat.descriptor, - LeaveChat.descriptor + LeaveChat.descriptor, + MuteChat.descriptor, + UnmuteChat.descriptor ] } } @@ -298,6 +324,62 @@ extension Flipcash_Chat_V1_Chat { options: GRPCCore.CallOptions, onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable + + /// Call the "MuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > MuteChat mutes a chat for the caller, until a time or indefinitely. + /// > + /// > Only a member may mute. Muting does not affect the event stream or + /// > message delivery: pushes for a muted chat are still sent, flagged with + /// > push.v1.ChatMetadata.muted, and the client suppresses the notification. + /// > Calling it again with a different duration replaces the mute. An + /// > identical request is a no-op. A mute is cleared when the caller leaves + /// > the chat. Every real change reaches the caller's other devices as a + /// > MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_MuteChatRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_MuteChatRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_MuteChatResponse` 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 muteChat( + 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 + + /// Call the "UnmuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > UnmuteChat clears the caller's mute on a chat. Unmuting a chat that is + /// > not muted is a no-op. Every real change reaches the caller's other devices + /// > as a MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_UnmuteChatRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_UnmuteChatRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_UnmuteChatResponse` 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 unmuteChat( + 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.chat.v1.Chat" service. @@ -561,6 +643,84 @@ extension Flipcash_Chat_V1_Chat { onResponse: handleResponse ) } + + /// Call the "MuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > MuteChat mutes a chat for the caller, until a time or indefinitely. + /// > + /// > Only a member may mute. Muting does not affect the event stream or + /// > message delivery: pushes for a muted chat are still sent, flagged with + /// > push.v1.ChatMetadata.muted, and the client suppresses the notification. + /// > Calling it again with a different duration replaces the mute. An + /// > identical request is a no-op. A mute is cleared when the caller leaves + /// > the chat. Every real change reaches the caller's other devices as a + /// > MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_MuteChatRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_MuteChatRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_MuteChatResponse` 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 muteChat( + 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_Chat_V1_Chat.Method.MuteChat.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "UnmuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > UnmuteChat clears the caller's mute on a chat. Unmuting a chat that is + /// > not muted is a no-op. Every real change reaches the caller's other devices + /// > as a MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_UnmuteChatRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_UnmuteChatRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_UnmuteChatResponse` 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 unmuteChat( + 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_Chat_V1_Chat.Method.UnmuteChat.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } } } @@ -782,6 +942,74 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { onResponse: handleResponse ) } + + /// Call the "MuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > MuteChat mutes a chat for the caller, until a time or indefinitely. + /// > + /// > Only a member may mute. Muting does not affect the event stream or + /// > message delivery: pushes for a muted chat are still sent, flagged with + /// > push.v1.ChatMetadata.muted, and the client suppresses the notification. + /// > Calling it again with a different duration replaces the mute. An + /// > identical request is a no-op. A mute is cleared when the caller leaves + /// > the chat. Every real change reaches the caller's other devices as a + /// > MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_MuteChatRequest` 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 muteChat( + 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.muteChat( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "UnmuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > UnmuteChat clears the caller's mute on a chat. Unmuting a chat that is + /// > not muted is a no-op. Every real change reaches the caller's other devices + /// > as a MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_UnmuteChatRequest` 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 unmuteChat( + 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.unmuteChat( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } } // Helpers providing sugared APIs for 'ClientProtocol' methods. @@ -1026,4 +1254,80 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { onResponse: handleResponse ) } + + /// Call the "MuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > MuteChat mutes a chat for the caller, until a time or indefinitely. + /// > + /// > Only a member may mute. Muting does not affect the event stream or + /// > message delivery: pushes for a muted chat are still sent, flagged with + /// > push.v1.ChatMetadata.muted, and the client suppresses the notification. + /// > Calling it again with a different duration replaces the mute. An + /// > identical request is a no-op. A mute is cleared when the caller leaves + /// > the chat. Every real change reaches the caller's other devices as a + /// > MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - 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 muteChat( + _ message: Flipcash_Chat_V1_MuteChatRequest, + 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.muteChat( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "UnmuteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > UnmuteChat clears the caller's mute on a chat. Unmuting a chat that is + /// > not muted is a no-op. Every real change reaches the caller's other devices + /// > as a MetadataUpdate.ViewerStateChanged on the event stream. + /// + /// - 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 unmuteChat( + _ message: Flipcash_Chat_V1_UnmuteChatRequest, + 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.unmuteChat( + request: request, + options: options, + onResponse: handleResponse + ) + } } \ No newline at end of file diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift index fd5d508..f5ba6ab 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift @@ -34,6 +34,16 @@ public struct Flipcash_Chat_V1_GetChatRequest: Sendable { /// Clears the value of `chatID`. Subsequent reads from it will return its default value. public mutating func clearChatID() {self._chatID = nil} + /// What the client intends to render of the chat's messaging state — its + /// last_message and latest_event_sequence — and so whether they may be + /// returned redacted (see messaging.v1.ViewMode). A group's record (title, + /// picture, rules, roster summary) is returned to every registered user + /// whatever the mode; the mode decides only the messaging state, which is + /// withheld from a viewer who may not read the chat under it. Unset (FULL) + /// is the pre-redaction contract: messaging state for a viewer who may + /// read the chat in full, the bare record for anyone else. + public var viewMode: Flipcash_Messaging_V1_ViewMode = .full + public var auth: Flipcash_Common_V1_Auth { get {return _auth ?? Flipcash_Common_V1_Auth()} set {_auth = newValue} @@ -703,13 +713,214 @@ public struct Flipcash_Chat_V1_LeaveChatResponse: Sendable { public init() {} } +public struct Flipcash_Chat_V1_MuteChatRequest: 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 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} + + /// How long to mute for. A timed mute must end in the future; the server + /// rejects one that does not. + public var mute: Flipcash_Chat_V1_MuteState { + get {return _mute ?? Flipcash_Chat_V1_MuteState()} + set {_mute = newValue} + } + /// Returns true if `mute` has been explicitly set. + public var hasMute: Bool {return self._mute != nil} + /// Clears the value of `mute`. Subsequent reads from it will return its default value. + public mutating func clearMute() {self._mute = nil} + + 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() + + public init() {} + + fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil + fileprivate var _mute: Flipcash_Chat_V1_MuteState? = nil + fileprivate var _auth: Flipcash_Common_V1_Auth? = nil +} + +public struct Flipcash_Chat_V1_MuteChatResponse: 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_Chat_V1_MuteChatResponse.Result = .ok + + /// The caller's state for the chat after the write, so the client can + /// apply it at its version. Set only when result == OK. + public var viewerState: Flipcash_Chat_V1_ViewerState { + get {return _viewerState ?? Flipcash_Chat_V1_ViewerState()} + set {_viewerState = newValue} + } + /// Returns true if `viewerState` has been explicitly set. + public var hasViewerState: Bool {return self._viewerState != nil} + /// Clears the value of `viewerState`. Subsequent reads from it will return its default value. + public mutating func clearViewerState() {self._viewerState = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum Result: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case ok // = 0 + + /// Caller is not a member + case denied // = 1 + case notFound // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .ok + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .denied + case 2: self = .notFound + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .ok: return 0 + case .denied: return 1 + case .notFound: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Flipcash_Chat_V1_MuteChatResponse.Result] = [ + .ok, + .denied, + .notFound, + ] + + } + + public init() {} + + fileprivate var _viewerState: Flipcash_Chat_V1_ViewerState? = nil +} + +public struct Flipcash_Chat_V1_UnmuteChatRequest: 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 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} + + 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() + + public init() {} + + fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil + fileprivate var _auth: Flipcash_Common_V1_Auth? = nil +} + +public struct Flipcash_Chat_V1_UnmuteChatResponse: 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_Chat_V1_UnmuteChatResponse.Result = .ok + + /// As for MuteChatResponse.viewer_state. + public var viewerState: Flipcash_Chat_V1_ViewerState { + get {return _viewerState ?? Flipcash_Chat_V1_ViewerState()} + set {_viewerState = newValue} + } + /// Returns true if `viewerState` has been explicitly set. + public var hasViewerState: Bool {return self._viewerState != nil} + /// Clears the value of `viewerState`. Subsequent reads from it will return its default value. + public mutating func clearViewerState() {self._viewerState = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum Result: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case ok // = 0 + case denied // = 1 + case notFound // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .ok + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .denied + case 2: self = .notFound + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .ok: return 0 + case .denied: return 1 + case .notFound: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Flipcash_Chat_V1_UnmuteChatResponse.Result] = [ + .ok, + .denied, + .notFound, + ] + + } + + public init() {} + + fileprivate var _viewerState: Flipcash_Chat_V1_ViewerState? = nil +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "flipcash.chat.v1" extension Flipcash_Chat_V1_GetChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GetChatRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{2}\u{9}auth\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}view_mode\0\u{2}\u{8}auth\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -718,6 +929,7 @@ extension Flipcash_Chat_V1_GetChatRequest: SwiftProtobuf.Message, SwiftProtobuf. // 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.decodeSingularEnumField(value: &self.viewMode) }() case 10: try { try decoder.decodeSingularMessageField(value: &self._auth) }() default: break } @@ -732,6 +944,9 @@ extension Flipcash_Chat_V1_GetChatRequest: SwiftProtobuf.Message, SwiftProtobuf. try { if let v = self._chatID { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) } }() + if self.viewMode != .full { + try visitor.visitSingularEnumField(value: self.viewMode, fieldNumber: 2) + } try { if let v = self._auth { try visitor.visitSingularMessageField(value: v, fieldNumber: 10) } }() @@ -740,6 +955,7 @@ extension Flipcash_Chat_V1_GetChatRequest: SwiftProtobuf.Message, SwiftProtobuf. public static func ==(lhs: Flipcash_Chat_V1_GetChatRequest, rhs: Flipcash_Chat_V1_GetChatRequest) -> Bool { if lhs._chatID != rhs._chatID {return false} + if lhs.viewMode != rhs.viewMode {return false} if lhs._auth != rhs._auth {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -1280,3 +1496,172 @@ extension Flipcash_Chat_V1_LeaveChatResponse: SwiftProtobuf.Message, SwiftProtob extension Flipcash_Chat_V1_LeaveChatResponse.Result: SwiftProtobuf._ProtoNameProviding { public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}NOT_FOUND\0") } + +extension Flipcash_Chat_V1_MuteChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".MuteChatRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}mute\0\u{2}\u{8}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 { try decoder.decodeSingularMessageField(value: &self._chatID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._mute) }() + 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 let v = self._chatID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._mute { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._auth { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MuteChatRequest, rhs: Flipcash_Chat_V1_MuteChatRequest) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs._mute != rhs._mute {return false} + if lhs._auth != rhs._auth {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_MuteChatResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".MuteChatResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{3}viewer_state\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._viewerState) }() + 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._viewerState { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MuteChatResponse, rhs: Flipcash_Chat_V1_MuteChatResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs._viewerState != rhs._viewerState {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_MuteChatResponse.Result: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}NOT_FOUND\0") +} + +extension Flipcash_Chat_V1_UnmuteChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".UnmuteChatRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\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 { try decoder.decodeSingularMessageField(value: &self._chatID) }() + 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 let v = self._chatID { + 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_Chat_V1_UnmuteChatRequest, rhs: Flipcash_Chat_V1_UnmuteChatRequest) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs._auth != rhs._auth {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_UnmuteChatResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".UnmuteChatResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{3}viewer_state\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._viewerState) }() + 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._viewerState { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_UnmuteChatResponse, rhs: Flipcash_Chat_V1_UnmuteChatResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs._viewerState != rhs._viewerState {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_UnmuteChatResponse.Result: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}NOT_FOUND\0") +} diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift index 2445f1e..acae12b 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift @@ -92,7 +92,10 @@ public struct Flipcash_Chat_V1_Metadata: @unchecked Sendable { set {_uniqueStorage()._members = newValue} } - /// The last message in this chat + /// The last message in this chat. Withheld, along with + /// latest_event_sequence, from a viewer who may not read the chat. May be + /// redacted (see messaging.v1.Message.redacted) when the read was made + /// under a messaging.v1.ViewMode other than FULL. public var lastMessage: Flipcash_Messaging_V1_Message { get {return _storage._lastMessage ?? Flipcash_Messaging_V1_Message()} set {_uniqueStorage()._lastMessage = newValue} @@ -171,6 +174,16 @@ public struct Flipcash_Chat_V1_Metadata: @unchecked Sendable { /// Clears the value of `rules`. Subsequent reads from it will return its default value. public mutating func clearRules() {_uniqueStorage()._rules = nil} + /// Per-viewer chat state, absent when the chat holds nothing about them. + public var viewerState: Flipcash_Chat_V1_ViewerState { + get {return _storage._viewerState ?? Flipcash_Chat_V1_ViewerState()} + set {_uniqueStorage()._viewerState = newValue} + } + /// Returns true if `viewerState` has been explicitly set. + public var hasViewerState: Bool {return _storage._viewerState != nil} + /// Clears the value of `viewerState`. Subsequent reads from it will return its default value. + public mutating func clearViewerState() {_uniqueStorage()._viewerState = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -375,7 +388,9 @@ public struct Flipcash_Chat_V1_RosterSummary: Sendable { /// Version of the roster: advanced by exactly one on every change to the /// membership records — a member joining, a member leaving, and in future /// any change to what the chat records about a member (e.g. a role) — and - /// never by an idempotent no-op or a profile change. + /// never by an idempotent no-op or a profile change. A viewer's own state + /// (see ViewerState) is not part of the roster: it is private to that + /// viewer, carries its own version, and never moves this one. /// /// Opaque to clients. Compare it against the last value seen: a different /// value means the cached member list may be stale and should be refetched. @@ -413,11 +428,20 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { set {kind = .lastActivityChanged(newValue)} } + public var viewerStateChanged: Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged { + get { + if case .viewerStateChanged(let v)? = kind {return v} + return Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged() + } + set {kind = .viewerStateChanged(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Kind: Equatable, Sendable { case fullRefresh(Flipcash_Chat_V1_MetadataUpdate.FullRefresh) case lastActivityChanged(Flipcash_Chat_V1_MetadataUpdate.LastActivityChanged) + case viewerStateChanged(Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged) } @@ -465,6 +489,28 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { fileprivate var _newLastActivity: SwiftProtobuf.Google_Protobuf_Timestamp? = nil } + /// The receiving user's own state for the chat has changed + public struct ViewerStateChanged: 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 viewerState: Flipcash_Chat_V1_ViewerState { + get {return _viewerState ?? Flipcash_Chat_V1_ViewerState()} + set {_viewerState = newValue} + } + /// Returns true if `viewerState` has been explicitly set. + public var hasViewerState: Bool {return self._viewerState != nil} + /// Clears the value of `viewerState`. Subsequent reads from it will return its default value. + public mutating func clearViewerState() {self._viewerState = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _viewerState: Flipcash_Chat_V1_ViewerState? = nil + } + public init() {} } @@ -611,6 +657,111 @@ public struct Flipcash_Chat_V1_RosterUpdateBatch: Sendable { public init() {} } +/// ViewerState is what this chat holds about the requesting user, +/// independent of whether they are a member. It is per-viewer and never +/// shared with other members. +public struct Flipcash_Chat_V1_ViewerState: 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. + + /// State the viewer set for themselves through their own RPCs. + public var settings: Flipcash_Chat_V1_ViewerState.Settings { + get {return _settings ?? Flipcash_Chat_V1_ViewerState.Settings()} + set {_settings = newValue} + } + /// Returns true if `settings` has been explicitly set. + public var hasSettings: Bool {return self._settings != nil} + /// Clears the value of `settings`. Subsequent reads from it will return its default value. + public mutating func clearSettings() {self._settings = nil} + + /// Advanced by exactly one on every real change to any field of this + /// message, never by a no-op. Compared like RosterSummary.version: apply + /// the greater value and drop the rest, so delivery order does not matter. + /// It is not the chat event sequence and has no delta. + public var version: UInt64 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public struct Settings: 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. + + /// Present if and only if the chat is muted for the viewer. Muted pushes + /// are still delivered, flagged with push.v1.ChatMetadata.muted, and the + /// client suppresses the notification. + public var mute: Flipcash_Chat_V1_MuteState { + get {return _mute ?? Flipcash_Chat_V1_MuteState()} + set {_mute = newValue} + } + /// Returns true if `mute` has been explicitly set. + public var hasMute: Bool {return self._mute != nil} + /// Clears the value of `mute`. Subsequent reads from it will return its default value. + public mutating func clearMute() {self._mute = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _mute: Flipcash_Chat_V1_MuteState? = nil + } + + public init() {} + + fileprivate var _settings: Flipcash_Chat_V1_ViewerState.Settings? = nil +} + +public struct Flipcash_Chat_V1_MuteState: 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 duration: Flipcash_Chat_V1_MuteState.OneOf_Duration? = nil + + /// Muted until this time, after which the mute lapses. Nothing is + /// sent when it does, so the client owns the countdown. + public var until: SwiftProtobuf.Google_Protobuf_Timestamp { + get { + if case .until(let v)? = duration {return v} + return SwiftProtobuf.Google_Protobuf_Timestamp() + } + set {duration = .until(newValue)} + } + + /// Muted forever until explicitly unmuted. + public var forever: Flipcash_Chat_V1_MuteState.Forever { + get { + if case .forever(let v)? = duration {return v} + return Flipcash_Chat_V1_MuteState.Forever() + } + set {duration = .forever(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_Duration: Equatable, Sendable { + /// Muted until this time, after which the mute lapses. Nothing is + /// sent when it does, so the client owns the countdown. + case until(SwiftProtobuf.Google_Protobuf_Timestamp) + /// Muted forever until explicitly unmuted. + case forever(Flipcash_Chat_V1_MuteState.Forever) + + } + + public struct Forever: 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 unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + + public init() {} +} + /// IdempotencyKey lets a client safely retry a request that creates something. /// The server derives the created object's identity from the caller and this /// key, so a retry with the same key returns the original result instead of @@ -641,7 +792,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{1}picture\0\u{3}roster_summary\0\u{1}rules\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{1}picture\0\u{3}roster_summary\0\u{1}rules\0\u{3}viewer_state\0") fileprivate class _StorageClass { var _chatID: Flipcash_Common_V1_ChatId? = nil @@ -655,6 +806,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa var _picture: Flipcash_Blob_V1_Media? = nil var _rosterSummary: Flipcash_Chat_V1_RosterSummary? = nil var _rules: Flipcash_Chat_V1_Rules? = nil + var _viewerState: Flipcash_Chat_V1_ViewerState? = nil // 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. @@ -676,6 +828,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa _picture = source._picture _rosterSummary = source._rosterSummary _rules = source._rules + _viewerState = source._viewerState } } @@ -705,6 +858,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa case 9: try { try decoder.decodeSingularMessageField(value: &_storage._picture) }() case 10: try { try decoder.decodeSingularMessageField(value: &_storage._rosterSummary) }() case 11: try { try decoder.decodeSingularMessageField(value: &_storage._rules) }() + case 12: try { try decoder.decodeSingularMessageField(value: &_storage._viewerState) }() default: break } } @@ -750,6 +904,9 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = _storage._rules { try visitor.visitSingularMessageField(value: v, fieldNumber: 11) } }() + try { if let v = _storage._viewerState { + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -770,6 +927,7 @@ extension Flipcash_Chat_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Messa if _storage._picture != rhs_storage._picture {return false} if _storage._rosterSummary != rhs_storage._rosterSummary {return false} if _storage._rules != rhs_storage._rules {return false} + if _storage._viewerState != rhs_storage._viewerState {return false} return true } if !storagesAreEqual {return false} @@ -1087,7 +1245,7 @@ extension Flipcash_Chat_V1_RosterSummary: SwiftProtobuf.Message, SwiftProtobuf._ 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") + 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") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1121,6 +1279,19 @@ extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf. self.kind = .lastActivityChanged(v) } }() + case 3: try { + var v: Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .viewerStateChanged(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .viewerStateChanged(v) + } + }() default: break } } @@ -1140,6 +1311,10 @@ extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf. guard case .lastActivityChanged(let v)? = self.kind else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 2) }() + case .viewerStateChanged?: try { + guard case .viewerStateChanged(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1220,6 +1395,40 @@ extension Flipcash_Chat_V1_MetadataUpdate.LastActivityChanged: SwiftProtobuf.Mes } } +extension Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_MetadataUpdate.protoMessageName + ".ViewerStateChanged" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}viewer_state\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._viewerState) }() + 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._viewerState { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged, rhs: Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged) -> Bool { + if lhs._viewerState != rhs._viewerState {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Flipcash_Chat_V1_RosterUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".RosterUpdate" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}member_joined\0\u{3}member_left\0\u{4}\u{8}roster_summary\0") @@ -1433,6 +1642,165 @@ extension Flipcash_Chat_V1_RosterUpdateBatch: SwiftProtobuf.Message, SwiftProtob } } +extension Flipcash_Chat_V1_ViewerState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ViewerState" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}settings\0\u{2}\u{9}version\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._settings) }() + case 10: try { try decoder.decodeSingularUInt64Field(value: &self.version) }() + 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._settings { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.version != 0 { + try visitor.visitSingularUInt64Field(value: self.version, fieldNumber: 10) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_ViewerState, rhs: Flipcash_Chat_V1_ViewerState) -> Bool { + if lhs._settings != rhs._settings {return false} + if lhs.version != rhs.version {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_ViewerState.Settings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_ViewerState.protoMessageName + ".Settings" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}mute\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._mute) }() + 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._mute { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_ViewerState.Settings, rhs: Flipcash_Chat_V1_ViewerState.Settings) -> Bool { + if lhs._mute != rhs._mute {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_MuteState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".MuteState" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}until\0\u{1}forever\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: SwiftProtobuf.Google_Protobuf_Timestamp? + var hadOneofValue = false + if let current = self.duration { + hadOneofValue = true + if case .until(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.duration = .until(v) + } + }() + case 2: try { + var v: Flipcash_Chat_V1_MuteState.Forever? + var hadOneofValue = false + if let current = self.duration { + hadOneofValue = true + if case .forever(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.duration = .forever(v) + } + }() + 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 + switch self.duration { + case .until?: try { + guard case .until(let v)? = self.duration else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .forever?: try { + guard case .forever(let v)? = self.duration else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MuteState, rhs: Flipcash_Chat_V1_MuteState) -> Bool { + if lhs.duration != rhs.duration {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_MuteState.Forever: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_MuteState.protoMessageName + ".Forever" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MuteState.Forever, rhs: Flipcash_Chat_V1_MuteState.Forever) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Flipcash_Chat_V1_IdempotencyKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".IdempotencyKey" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}value\0") diff --git a/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.grpc.swift b/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.grpc.swift index 5911fed..8bd325f 100644 --- a/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.grpc.swift +++ b/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.grpc.swift @@ -71,7 +71,24 @@ extension Flipcash_Event_V1_EventStreaming { /// /// > Source IDL Documentation: /// > - /// > StreamEvents streams events for the requesting user. + /// > StreamEvents streams events to the requesting user. Params, the first + /// > message on the stream, selects what is streamed: + /// > + /// > - With no target set, every event addressed to the signing user, across + /// > all of their events. This stream lives until either side closes it. + /// > - With chat_preview set, ChatUpdate for that single group chat, under + /// > a messaging.v1.ViewMode, and nothing else, for a bounded window of + /// > time. This is how a viewer who is not a member of a group — and so is + /// > not addressed by its events — previews it live before joining, and + /// > how any viewer previews a group redacted. It is the only + /// > chat-targeted stream; there is no open-ended stream for a single + /// > chat, and no stream of any kind targeted at a DM. + /// > + /// > The two are independent. A client may hold several streams at once, e.g. + /// > a user stream plus a preview stream for the group it is currently + /// > previewing as a non-member. Nothing is deduplicated across streams: a + /// > member who opens a preview stream for one of their own chats receives + /// > that chat's updates on both. /// /// - Parameters: /// - request: A streaming request producing `Flipcash_Event_V1_StreamEventsRequest` messages. @@ -134,7 +151,24 @@ extension Flipcash_Event_V1_EventStreaming { /// /// > Source IDL Documentation: /// > - /// > StreamEvents streams events for the requesting user. + /// > StreamEvents streams events to the requesting user. Params, the first + /// > message on the stream, selects what is streamed: + /// > + /// > - With no target set, every event addressed to the signing user, across + /// > all of their events. This stream lives until either side closes it. + /// > - With chat_preview set, ChatUpdate for that single group chat, under + /// > a messaging.v1.ViewMode, and nothing else, for a bounded window of + /// > time. This is how a viewer who is not a member of a group — and so is + /// > not addressed by its events — previews it live before joining, and + /// > how any viewer previews a group redacted. It is the only + /// > chat-targeted stream; there is no open-ended stream for a single + /// > chat, and no stream of any kind targeted at a DM. + /// > + /// > The two are independent. A client may hold several streams at once, e.g. + /// > a user stream plus a preview stream for the group it is currently + /// > previewing as a non-member. Nothing is deduplicated across streams: a + /// > member who opens a preview stream for one of their own chats receives + /// > that chat's updates on both. /// /// - Parameters: /// - request: A streaming request producing `Flipcash_Event_V1_StreamEventsRequest` messages. @@ -205,7 +239,24 @@ extension Flipcash_Event_V1_EventStreaming.ClientProtocol { /// /// > Source IDL Documentation: /// > - /// > StreamEvents streams events for the requesting user. + /// > StreamEvents streams events to the requesting user. Params, the first + /// > message on the stream, selects what is streamed: + /// > + /// > - With no target set, every event addressed to the signing user, across + /// > all of their events. This stream lives until either side closes it. + /// > - With chat_preview set, ChatUpdate for that single group chat, under + /// > a messaging.v1.ViewMode, and nothing else, for a bounded window of + /// > time. This is how a viewer who is not a member of a group — and so is + /// > not addressed by its events — previews it live before joining, and + /// > how any viewer previews a group redacted. It is the only + /// > chat-targeted stream; there is no open-ended stream for a single + /// > chat, and no stream of any kind targeted at a DM. + /// > + /// > The two are independent. A client may hold several streams at once, e.g. + /// > a user stream plus a preview stream for the group it is currently + /// > previewing as a non-member. Nothing is deduplicated across streams: a + /// > member who opens a preview stream for one of their own chats receives + /// > that chat's updates on both. /// /// - Parameters: /// - request: A streaming request producing `Flipcash_Event_V1_StreamEventsRequest` messages. @@ -265,7 +316,24 @@ extension Flipcash_Event_V1_EventStreaming.ClientProtocol { /// /// > Source IDL Documentation: /// > - /// > StreamEvents streams events for the requesting user. + /// > StreamEvents streams events to the requesting user. Params, the first + /// > message on the stream, selects what is streamed: + /// > + /// > - With no target set, every event addressed to the signing user, across + /// > all of their events. This stream lives until either side closes it. + /// > - With chat_preview set, ChatUpdate for that single group chat, under + /// > a messaging.v1.ViewMode, and nothing else, for a bounded window of + /// > time. This is how a viewer who is not a member of a group — and so is + /// > not addressed by its events — previews it live before joining, and + /// > how any viewer previews a group redacted. It is the only + /// > chat-targeted stream; there is no open-ended stream for a single + /// > chat, and no stream of any kind targeted at a DM. + /// > + /// > The two are independent. A client may hold several streams at once, e.g. + /// > a user stream plus a preview stream for the group it is currently + /// > previewing as a non-member. Nothing is deduplicated across streams: a + /// > member who opens a preview stream for one of their own chats receives + /// > that chat's updates on both. /// /// - Parameters: /// - metadata: Additional metadata to send, defaults to empty. diff --git a/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.pb.swift b/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.pb.swift index cbed58c..d97b9de 100644 --- a/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/event_v1_event_streaming_service.pb.swift @@ -78,14 +78,115 @@ public struct Flipcash_Event_V1_StreamEventsRequest: Sendable { /// Clears the value of `ts`. Subsequent reads from it will return its default value. public mutating func clearTs() {self._ts = nil} + /// What the stream is for. Optional: when nothing is set, the stream is + /// for the signing user and carries every event addressed to them, the + /// contract that predates this field. When set, it takes over, and the + /// stream carries only what the target describes. + public var target: Flipcash_Event_V1_StreamEventsRequest.Params.OneOf_Target? = nil + + public var chatPreview: Flipcash_Event_V1_StreamEventsRequest.ChatPreviewParams { + get { + if case .chatPreview(let v)? = target {return v} + return Flipcash_Event_V1_StreamEventsRequest.ChatPreviewParams() + } + set {target = .chatPreview(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() + /// What the stream is for. Optional: when nothing is set, the stream is + /// for the signing user and carries every event addressed to them, the + /// contract that predates this field. When set, it takes over, and the + /// stream carries only what the target describes. + public enum OneOf_Target: Equatable, Sendable { + case chatPreview(Flipcash_Event_V1_StreamEventsRequest.ChatPreviewParams) + + } + public init() {} fileprivate var _auth: Flipcash_Common_V1_Auth? = nil fileprivate var _ts: SwiftProtobuf.Google_Protobuf_Timestamp? = nil } + /// ChatPreviewParams targets a stream at a preview of a single group chat: + /// ChatUpdate for chat_id, under view_mode, and nothing else, for a bounded + /// window of time. No update for any other chat, is ever delivered on it. + /// + /// Only group chats can be previewed. A DM has no one to preview it — a + /// non-member of a DM may not read it under any mode — so a chat_id naming + /// a DM is DENIED, whatever the viewer's standing and whatever the mode, + /// by the same rule that denies every other read of it. A DM's members + /// follow it on their user stream. + /// + /// The stream is time-bounded. The server fixes the lifetime of a preview + /// at open — the bound is the server's, not the client's, and there is no + /// way to request a longer one — and ends the stream with STREAM_EXPIRED + /// when it elapses, whether or not anything was delivered. Pings and pongs + /// keep the stream healthy, not alive: a pong does not extend the window. + /// A client still previewing the chat when the window closes opens a new + /// preview stream, and reconciles anything it missed in between the way it + /// would after any reconnect (GetDelta for the event log, a refresh of the + /// overlays). The server may refuse to open a preview, with DENIED, to a + /// viewer who opens too many in too short a time. + /// + /// A preview is for a viewer who is not (yet) a member of the chat. A + /// member's own chats are already carried on their user stream, which is + /// not time-bounded, and a client should not preview a chat it is a member + /// of; the server does not refuse it, but delivers the chat's updates on + /// both streams. A viewer who joins the chat while previewing it switches + /// to their user stream: the preview keeps delivering until its window + /// closes, but is not extended by the join. + /// + /// The stream opens only if the viewer may read the chat under the mode, + /// by the same rule as any other read under a messaging.v1.ViewMode: a + /// member, or a non-member of a group who satisfies its listener rules, + /// may read in full; a non-member of a group with listener rules they do + /// not satisfy may read redacted; anyone else is DENIED. A chat that does + /// not exist is NOT_FOUND. Standing is evaluated at stream open, and the + /// server may end the stream with DENIED before the window closes if the + /// viewer later may not read the chat under the mode — e.g. the rules + /// changed, or they were removed from the roster. + /// + /// Within a ChatUpdate, the mode decides only the messages: those carried + /// by events, and last_message in metadata_updates. Every message on the + /// stream is delivered under the mode fixed at open, like GetDelta, never + /// mixed, with Message.redacted set on each when redacted. Pointer, + /// typing, reaction and roster overlays are delivered whatever the mode. + /// + /// A client previewing a chat redacted keeps this stream and its + /// GetMessages/GetDelta reads under the same mode, so the events it + /// applies are shaped like the state it applies them to (see + /// messaging.v1.Message.redacted). + public struct ChatPreviewParams: 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 one chat whose updates are streamed. Must be a group chat; a DM + /// is DENIED. + 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} + + /// What the client intends to render of the chat, and so whether its + /// messages may be delivered redacted (see messaging.v1.ViewMode). + /// Unset (FULL) delivers full content to a viewer who may read the + /// chat in full, and is DENIED to everyone else. + public var viewMode: Flipcash_Messaging_V1_ViewMode = .full + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil + } + public init() {} } @@ -142,6 +243,8 @@ public struct Flipcash_Event_V1_StreamEventsResponse: Sendable { public typealias RawValue = Int case denied // = 0 case invalidTimestamp // = 1 + case notFound // = 2 + case streamExpired // = 3 case UNRECOGNIZED(Int) public init() { @@ -152,6 +255,8 @@ public struct Flipcash_Event_V1_StreamEventsResponse: Sendable { switch rawValue { case 0: self = .denied case 1: self = .invalidTimestamp + case 2: self = .notFound + case 3: self = .streamExpired default: self = .UNRECOGNIZED(rawValue) } } @@ -160,6 +265,8 @@ public struct Flipcash_Event_V1_StreamEventsResponse: Sendable { switch self { case .denied: return 0 case .invalidTimestamp: return 1 + case .notFound: return 2 + case .streamExpired: return 3 case .UNRECOGNIZED(let i): return i } } @@ -168,6 +275,8 @@ public struct Flipcash_Event_V1_StreamEventsResponse: Sendable { public static let allCases: [Flipcash_Event_V1_StreamEventsResponse.StreamError.Code] = [ .denied, .invalidTimestamp, + .notFound, + .streamExpired, ] } @@ -331,7 +440,7 @@ extension Flipcash_Event_V1_StreamEventsRequest: SwiftProtobuf.Message, SwiftPro extension Flipcash_Event_V1_StreamEventsRequest.Params: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Flipcash_Event_V1_StreamEventsRequest.protoMessageName + ".Params" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}auth\0\u{1}ts\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}auth\0\u{1}ts\0\u{3}chat_preview\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -341,6 +450,19 @@ extension Flipcash_Event_V1_StreamEventsRequest.Params: SwiftProtobuf.Message, S switch fieldNumber { case 1: try { try decoder.decodeSingularMessageField(value: &self._auth) }() case 2: try { try decoder.decodeSingularMessageField(value: &self._ts) }() + case 3: try { + var v: Flipcash_Event_V1_StreamEventsRequest.ChatPreviewParams? + var hadOneofValue = false + if let current = self.target { + hadOneofValue = true + if case .chatPreview(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.target = .chatPreview(v) + } + }() default: break } } @@ -357,12 +479,55 @@ extension Flipcash_Event_V1_StreamEventsRequest.Params: SwiftProtobuf.Message, S try { if let v = self._ts { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() + try { if case .chatPreview(let v)? = self.target { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Event_V1_StreamEventsRequest.Params, rhs: Flipcash_Event_V1_StreamEventsRequest.Params) -> Bool { if lhs._auth != rhs._auth {return false} if lhs._ts != rhs._ts {return false} + if lhs.target != rhs.target {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Event_V1_StreamEventsRequest.ChatPreviewParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Event_V1_StreamEventsRequest.protoMessageName + ".ChatPreviewParams" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}view_mode\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.decodeSingularEnumField(value: &self.viewMode) }() + 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) + } }() + if self.viewMode != .full { + try visitor.visitSingularEnumField(value: self.viewMode, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Event_V1_StreamEventsRequest.ChatPreviewParams, rhs: Flipcash_Event_V1_StreamEventsRequest.ChatPreviewParams) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs.viewMode != rhs.viewMode {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -483,7 +648,7 @@ extension Flipcash_Event_V1_StreamEventsResponse.StreamError: SwiftProtobuf.Mess } extension Flipcash_Event_V1_StreamEventsResponse.StreamError.Code: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DENIED\0\u{1}INVALID_TIMESTAMP\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DENIED\0\u{1}INVALID_TIMESTAMP\0\u{1}NOT_FOUND\0\u{1}STREAM_EXPIRED\0") } extension Flipcash_Event_V1_ForwardEventsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { diff --git a/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift index 5010e72..f11c86e 100644 --- a/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift @@ -327,7 +327,7 @@ public struct Flipcash_Event_V1_ChatUpdate: Sendable { /// If present, best-effort real-time reaction changes for messages in the /// chat. Like pointer_updates, reactions are a convergent overlay — NOT part /// of the gap-detected event log; clients apply them last-writer-wins by - /// ReactionUpdate.sequence and reconcile any misses by refreshing a message's + /// ReactionUpdate.version and reconcile any misses by refreshing a message's /// ReactionSummary on view. public var reactionUpdates: Flipcash_Messaging_V1_ReactionUpdateBatch { get {return _reactionUpdates ?? Flipcash_Messaging_V1_ReactionUpdateBatch()} diff --git a/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.grpc.swift b/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.grpc.swift index 0931f50..e2d268f 100644 --- a/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.grpc.swift +++ b/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.grpc.swift @@ -437,6 +437,17 @@ extension Flipcash_Messaging_V1_Messaging { /// > GetReactors returns the paged list of users who reacted to a message with /// > a given emoji — the on-demand drill-down behind EmojiReaction.count, which /// > never inlines the full reactor list. + /// > + /// > The reaction reads (GetReactors, GetReactionSummary, GetReactionSummaries) + /// > carry no ViewMode, since they return no Message, and are answered for + /// > anyone who may read the chat under any mode: a member, or a non-member of + /// > a group that carries a listener rule, whether or not they satisfy it. + /// > Reactions are an overlay — who reacted, with what, on which message is + /// > the conversation's movement, not its words — and are delivered whole to a + /// > redacted reader, on these reads as on a chat preview stream (see + /// > event.v1.StreamEventsRequest.ChatPreviewParams). A non-member's + /// > self_reactor is always absent: reacting is a member's write. Anyone who + /// > may not read the chat at all is DENIED. /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactorsRequest` message. @@ -460,7 +471,8 @@ extension Flipcash_Messaging_V1_Messaging { /// > Source IDL Documentation: /// > /// > GetReactionSummary fetches the current aggregate reaction state for a - /// > single message. + /// > single message. Answered for anyone who may read the chat under any mode + /// > (see GetReactors). /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactionSummaryRequest` message. @@ -484,7 +496,8 @@ extension Flipcash_Messaging_V1_Messaging { /// > Source IDL Documentation: /// > /// > GetReactionSummaries fetches the current aggregate reaction state using - /// > paged and batched APIs + /// > paged and batched APIs. Answered for anyone who may read the chat under + /// > any mode (see GetReactors). /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactionSummariesRequest` message. @@ -881,6 +894,17 @@ extension Flipcash_Messaging_V1_Messaging { /// > GetReactors returns the paged list of users who reacted to a message with /// > a given emoji — the on-demand drill-down behind EmojiReaction.count, which /// > never inlines the full reactor list. + /// > + /// > The reaction reads (GetReactors, GetReactionSummary, GetReactionSummaries) + /// > carry no ViewMode, since they return no Message, and are answered for + /// > anyone who may read the chat under any mode: a member, or a non-member of + /// > a group that carries a listener rule, whether or not they satisfy it. + /// > Reactions are an overlay — who reacted, with what, on which message is + /// > the conversation's movement, not its words — and are delivered whole to a + /// > redacted reader, on these reads as on a chat preview stream (see + /// > event.v1.StreamEventsRequest.ChatPreviewParams). A non-member's + /// > self_reactor is always absent: reacting is a member's write. Anyone who + /// > may not read the chat at all is DENIED. /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactorsRequest` message. @@ -915,7 +939,8 @@ extension Flipcash_Messaging_V1_Messaging { /// > Source IDL Documentation: /// > /// > GetReactionSummary fetches the current aggregate reaction state for a - /// > single message. + /// > single message. Answered for anyone who may read the chat under any mode + /// > (see GetReactors). /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactionSummaryRequest` message. @@ -950,7 +975,8 @@ extension Flipcash_Messaging_V1_Messaging { /// > Source IDL Documentation: /// > /// > GetReactionSummaries fetches the current aggregate reaction state using - /// > paged and batched APIs + /// > paged and batched APIs. Answered for anyone who may read the chat under + /// > any mode (see GetReactors). /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactionSummariesRequest` message. @@ -1328,6 +1354,17 @@ extension Flipcash_Messaging_V1_Messaging.ClientProtocol { /// > GetReactors returns the paged list of users who reacted to a message with /// > a given emoji — the on-demand drill-down behind EmojiReaction.count, which /// > never inlines the full reactor list. + /// > + /// > The reaction reads (GetReactors, GetReactionSummary, GetReactionSummaries) + /// > carry no ViewMode, since they return no Message, and are answered for + /// > anyone who may read the chat under any mode: a member, or a non-member of + /// > a group that carries a listener rule, whether or not they satisfy it. + /// > Reactions are an overlay — who reacted, with what, on which message is + /// > the conversation's movement, not its words — and are delivered whole to a + /// > redacted reader, on these reads as on a chat preview stream (see + /// > event.v1.StreamEventsRequest.ChatPreviewParams). A non-member's + /// > self_reactor is always absent: reacting is a member's write. Anyone who + /// > may not read the chat at all is DENIED. /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactorsRequest` message. @@ -1357,7 +1394,8 @@ extension Flipcash_Messaging_V1_Messaging.ClientProtocol { /// > Source IDL Documentation: /// > /// > GetReactionSummary fetches the current aggregate reaction state for a - /// > single message. + /// > single message. Answered for anyone who may read the chat under any mode + /// > (see GetReactors). /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactionSummaryRequest` message. @@ -1387,7 +1425,8 @@ extension Flipcash_Messaging_V1_Messaging.ClientProtocol { /// > Source IDL Documentation: /// > /// > GetReactionSummaries fetches the current aggregate reaction state using - /// > paged and batched APIs + /// > paged and batched APIs. Answered for anyone who may read the chat under + /// > any mode (see GetReactors). /// /// - Parameters: /// - request: A request containing a single `Flipcash_Messaging_V1_GetReactionSummariesRequest` message. @@ -1781,6 +1820,17 @@ extension Flipcash_Messaging_V1_Messaging.ClientProtocol { /// > GetReactors returns the paged list of users who reacted to a message with /// > a given emoji — the on-demand drill-down behind EmojiReaction.count, which /// > never inlines the full reactor list. + /// > + /// > The reaction reads (GetReactors, GetReactionSummary, GetReactionSummaries) + /// > carry no ViewMode, since they return no Message, and are answered for + /// > anyone who may read the chat under any mode: a member, or a non-member of + /// > a group that carries a listener rule, whether or not they satisfy it. + /// > Reactions are an overlay — who reacted, with what, on which message is + /// > the conversation's movement, not its words — and are delivered whole to a + /// > redacted reader, on these reads as on a chat preview stream (see + /// > event.v1.StreamEventsRequest.ChatPreviewParams). A non-member's + /// > self_reactor is always absent: reacting is a member's write. Anyone who + /// > may not read the chat at all is DENIED. /// /// - Parameters: /// - message: request message to send. @@ -1814,7 +1864,8 @@ extension Flipcash_Messaging_V1_Messaging.ClientProtocol { /// > Source IDL Documentation: /// > /// > GetReactionSummary fetches the current aggregate reaction state for a - /// > single message. + /// > single message. Answered for anyone who may read the chat under any mode + /// > (see GetReactors). /// /// - Parameters: /// - message: request message to send. @@ -1848,7 +1899,8 @@ extension Flipcash_Messaging_V1_Messaging.ClientProtocol { /// > Source IDL Documentation: /// > /// > GetReactionSummaries fetches the current aggregate reaction state using - /// > paged and batched APIs + /// > paged and batched APIs. Answered for anyone who may read the chat under + /// > any mode (see GetReactors). /// /// - Parameters: /// - message: request message to send. diff --git a/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift b/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift index 7af2d7e..3ff609d 100644 --- a/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/messaging_v1_messaging_service.pb.swift @@ -43,6 +43,11 @@ public struct Flipcash_Messaging_V1_GetMessageRequest: Sendable { /// Clears the value of `messageID`. Subsequent reads from it will return its default value. public mutating func clearMessageID() {self._messageID = nil} + /// What the client intends to render, and so whether the message may be + /// returned redacted (see ViewMode). Unset (FULL) is the pre-redaction + /// contract: full content, or DENIED. + public var viewMode: Flipcash_Messaging_V1_ViewMode = .full + public var auth: Flipcash_Common_V1_Auth { get {return _auth ?? Flipcash_Common_V1_Auth()} set {_auth = newValue} @@ -154,6 +159,13 @@ public struct Flipcash_Messaging_V1_GetMessagesRequest: Sendable { set {query = .messageIds(newValue)} } + /// What the client intends to render, and so whether the messages may be + /// returned redacted (see ViewMode). Applies to both query forms. Unset + /// (FULL) is the pre-redaction contract: full content, or DENIED. Every + /// message in a response is returned under the same mode: a page is + /// either wholly full or wholly redacted, never mixed. + public var viewMode: Flipcash_Messaging_V1_ViewMode = .full + public var auth: Flipcash_Common_V1_Auth { get {return _auth ?? Flipcash_Common_V1_Auth()} set {_auth = newValue} @@ -258,6 +270,15 @@ public struct Flipcash_Messaging_V1_GetDeltaRequest: Sendable { /// the retained log. public var afterSequence: UInt64 = 0 + /// What the client intends to render, and so whether the messages may be + /// returned redacted (see ViewMode). Unset (FULL) is the pre-redaction + /// contract: full content, or DENIED. The mode is fixed at stream open, + /// like latest_sequence: every batch in the stream is returned under the + /// same mode, never mixed. A client catching up a redacted view must use + /// the same mode it read the history under, so the delta it applies is + /// shaped like the state it applies it to. + public var viewMode: Flipcash_Messaging_V1_ViewMode = .full + public var auth: Flipcash_Common_V1_Auth { get {return _auth ?? Flipcash_Common_V1_Auth()} set {_auth = newValue} @@ -790,7 +811,8 @@ public struct Flipcash_Messaging_V1_AddReactionResponse: Sendable { public var result: Flipcash_Messaging_V1_AddReactionResponse.Result = .ok - /// The affected emoji's aggregate after the add (count, reacted_by_self true). + /// The affected emoji's aggregate after the add (count, self_reactor set at + /// the aggregate's version). public var reaction: Flipcash_Messaging_V1_EmojiReaction { get {return _reaction ?? Flipcash_Messaging_V1_EmojiReaction()} set {_reaction = newValue} @@ -1003,11 +1025,13 @@ 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 (server-ordered, typically most-recent - /// first). 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. + /// Paging over the reactor list. The order is fixed: 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. public var options: Flipcash_Common_V1_QueryOptions { get {return _options ?? Flipcash_Common_V1_QueryOptions()} set {_options = newValue} @@ -1044,8 +1068,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, with their reaction - /// timestamps. Empty when the message exists but has no reactors for the emoji. + /// 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. public var reactors: [Flipcash_Messaging_V1_Reactor] = [] /// The server-generated cursor advanced past this page. The client MUST send @@ -1066,6 +1091,16 @@ public struct Flipcash_Messaging_V1_GetReactorsResponse: Sendable { /// another GetReactorsRequest with the returned paging_token. public var hasMore_p: Bool = false + /// The emoji aggregate's version (EmojiReaction.version) the page is current + /// to: the server reads it BEFORE the page, so the page reflects every add + /// and remove up to this version and possibly some after it. A client + /// 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. + public var version: UInt64 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum Result: SwiftProtobuf.Enum, Swift.CaseIterable { @@ -1159,9 +1194,9 @@ public struct Flipcash_Messaging_V1_GetReactionSummaryResponse: Sendable { public var result: Flipcash_Messaging_V1_GetReactionSummaryResponse.Result = .ok - /// The aggregate reaction state for the message. reacted_by_self is computed + /// The aggregate reaction state for the message. self_reactor is computed /// for the caller; clients still apply per (message, emoji) by - /// EmojiReaction.sequence, so a summary that is slightly behind a live update + /// EmojiReaction.version, so a summary that is slightly behind a live update /// is harmlessly ignored rather than regressing state. public var summary: Flipcash_Messaging_V1_ReactionSummary { get {return _summary ?? Flipcash_Messaging_V1_ReactionSummary()} @@ -1280,9 +1315,9 @@ public struct Flipcash_Messaging_V1_GetReactionSummariesResponse: Sendable { public var result: Flipcash_Messaging_V1_GetReactionSummariesResponse.Result = .ok /// One summary per requested message, keyed by ReactionSummary.message_id. - /// reacted_by_self in each summary is computed for the caller; clients still - /// apply per (message, emoji) by EmojiReaction.sequence, so a summary that - /// is slightly behind a live update is harmlessly ignored rather than regressing + /// self_reactor in each summary is computed for the caller; clients still + /// apply per (message, emoji) by EmojiReaction.version, so a summary that is + /// slightly behind a live update is harmlessly ignored rather than regressing /// state. public var summaries: [Flipcash_Messaging_V1_ReactionSummary] = [] @@ -1503,7 +1538,7 @@ fileprivate let _protobuf_package = "flipcash.messaging.v1" extension Flipcash_Messaging_V1_GetMessageRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GetMessageRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}message_id\0\u{2}\u{8}auth\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}message_id\0\u{3}view_mode\0\u{2}\u{7}auth\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1513,6 +1548,7 @@ extension Flipcash_Messaging_V1_GetMessageRequest: SwiftProtobuf.Message, SwiftP 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.decodeSingularEnumField(value: &self.viewMode) }() case 10: try { try decoder.decodeSingularMessageField(value: &self._auth) }() default: break } @@ -1530,6 +1566,9 @@ extension Flipcash_Messaging_V1_GetMessageRequest: SwiftProtobuf.Message, SwiftP try { if let v = self._messageID { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() + if self.viewMode != .full { + try visitor.visitSingularEnumField(value: self.viewMode, fieldNumber: 3) + } try { if let v = self._auth { try visitor.visitSingularMessageField(value: v, fieldNumber: 10) } }() @@ -1539,6 +1578,7 @@ extension Flipcash_Messaging_V1_GetMessageRequest: SwiftProtobuf.Message, SwiftP public static func ==(lhs: Flipcash_Messaging_V1_GetMessageRequest, rhs: Flipcash_Messaging_V1_GetMessageRequest) -> Bool { if lhs._chatID != rhs._chatID {return false} if lhs._messageID != rhs._messageID {return false} + if lhs.viewMode != rhs.viewMode {return false} if lhs._auth != rhs._auth {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -1590,7 +1630,7 @@ extension Flipcash_Messaging_V1_GetMessageResponse.Result: SwiftProtobuf._ProtoN extension Flipcash_Messaging_V1_GetMessagesRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GetMessagesRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}options\0\u{3}message_ids\0\u{2}\u{7}auth\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}options\0\u{3}message_ids\0\u{3}view_mode\0\u{2}\u{6}auth\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1625,6 +1665,7 @@ extension Flipcash_Messaging_V1_GetMessagesRequest: SwiftProtobuf.Message, Swift self.query = .messageIds(v) } }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.viewMode) }() case 10: try { try decoder.decodeSingularMessageField(value: &self._auth) }() default: break } @@ -1650,6 +1691,9 @@ extension Flipcash_Messaging_V1_GetMessagesRequest: SwiftProtobuf.Message, Swift }() case nil: break } + if self.viewMode != .full { + try visitor.visitSingularEnumField(value: self.viewMode, fieldNumber: 4) + } try { if let v = self._auth { try visitor.visitSingularMessageField(value: v, fieldNumber: 10) } }() @@ -1659,6 +1703,7 @@ extension Flipcash_Messaging_V1_GetMessagesRequest: SwiftProtobuf.Message, Swift public static func ==(lhs: Flipcash_Messaging_V1_GetMessagesRequest, rhs: Flipcash_Messaging_V1_GetMessagesRequest) -> Bool { if lhs._chatID != rhs._chatID {return false} if lhs.query != rhs.query {return false} + if lhs.viewMode != rhs.viewMode {return false} if lhs._auth != rhs._auth {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -1710,7 +1755,7 @@ extension Flipcash_Messaging_V1_GetMessagesResponse.Result: SwiftProtobuf._Proto extension Flipcash_Messaging_V1_GetDeltaRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GetDeltaRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}after_sequence\0\u{2}\u{8}auth\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}after_sequence\0\u{3}view_mode\0\u{2}\u{7}auth\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1720,6 +1765,7 @@ extension Flipcash_Messaging_V1_GetDeltaRequest: SwiftProtobuf.Message, SwiftPro switch fieldNumber { case 1: try { try decoder.decodeSingularMessageField(value: &self._chatID) }() case 2: try { try decoder.decodeSingularUInt64Field(value: &self.afterSequence) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.viewMode) }() case 10: try { try decoder.decodeSingularMessageField(value: &self._auth) }() default: break } @@ -1737,6 +1783,9 @@ extension Flipcash_Messaging_V1_GetDeltaRequest: SwiftProtobuf.Message, SwiftPro if self.afterSequence != 0 { try visitor.visitSingularUInt64Field(value: self.afterSequence, fieldNumber: 2) } + if self.viewMode != .full { + try visitor.visitSingularEnumField(value: self.viewMode, fieldNumber: 3) + } try { if let v = self._auth { try visitor.visitSingularMessageField(value: v, fieldNumber: 10) } }() @@ -1746,6 +1795,7 @@ extension Flipcash_Messaging_V1_GetDeltaRequest: SwiftProtobuf.Message, SwiftPro public static func ==(lhs: Flipcash_Messaging_V1_GetDeltaRequest, rhs: Flipcash_Messaging_V1_GetDeltaRequest) -> Bool { if lhs._chatID != rhs._chatID {return false} if lhs.afterSequence != rhs.afterSequence {return false} + if lhs.viewMode != rhs.viewMode {return false} if lhs._auth != rhs._auth {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -2326,7 +2376,7 @@ extension Flipcash_Messaging_V1_GetReactorsRequest: SwiftProtobuf.Message, Swift extension Flipcash_Messaging_V1_GetReactorsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GetReactorsResponse" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{1}reactors\0\u{3}paging_token\0\u{3}has_more\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{1}reactors\0\u{3}paging_token\0\u{3}has_more\0\u{1}version\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2338,6 +2388,7 @@ extension Flipcash_Messaging_V1_GetReactorsResponse: SwiftProtobuf.Message, Swif case 2: try { try decoder.decodeRepeatedMessageField(value: &self.reactors) }() case 3: try { try decoder.decodeSingularMessageField(value: &self._pagingToken) }() case 4: try { try decoder.decodeSingularBoolField(value: &self.hasMore_p) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self.version) }() default: break } } @@ -2360,6 +2411,9 @@ extension Flipcash_Messaging_V1_GetReactorsResponse: SwiftProtobuf.Message, Swif if self.hasMore_p != false { try visitor.visitSingularBoolField(value: self.hasMore_p, fieldNumber: 4) } + if self.version != 0 { + try visitor.visitSingularUInt64Field(value: self.version, fieldNumber: 5) + } try unknownFields.traverse(visitor: &visitor) } @@ -2368,6 +2422,7 @@ extension Flipcash_Messaging_V1_GetReactorsResponse: SwiftProtobuf.Message, Swif if lhs.reactors != rhs.reactors {return false} if lhs._pagingToken != rhs._pagingToken {return false} if lhs.hasMore_p != rhs.hasMore_p {return false} + if lhs.version != rhs.version {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift index 6836e82..cc25341 100644 --- a/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/messaging_v1_model.pb.swift @@ -21,6 +21,84 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } +/// ViewMode is what a client intends to render from a read of a chat's +/// messages, and so how much of them the server may return. It is set on +/// every read that returns a Message (GetMessage, GetMessages, GetDelta, +/// chat.v1.GetChat, for last_message, and event.v1.StreamEvents when +/// previewing a chat) and is combined with the viewer's standing in the chat: +/// +/// - A member, or a non-member of a group who satisfies its listener rules, +/// may read the chat in full. +/// - A non-member of a group who does not satisfy its listener rules may +/// read it redacted, provided the group has at least one listener rule. +/// - Anyone else — a non-member of a DM, or of a group with no listener +/// rules — may not read it at all. +/// +/// The mode never widens what the standing allows: it can turn a full read +/// into a redacted one, or a refusal into a redacted read, but never a +/// redacted read into a full one. Redaction is the server's, not the +/// client's; blurring full content on the device is not a substitute, since +/// the content is still on the device. +/// +/// The default, FULL, is the contract that predates redaction, so a client +/// that does not set the field behaves exactly as before. +public enum Flipcash_Messaging_V1_ViewMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// Full content or nothing. A viewer who may read the chat in full gets + /// it; every other viewer is DENIED. Never returns a redacted message, so + /// a client that does not understand Message.redacted is never handed one. + case full // = 0 + + /// The most the viewer's standing allows. A viewer who may read the chat + /// in full gets it; a viewer who may only read it redacted gets it + /// redacted, with Message.redacted set on every message; every other + /// viewer is DENIED. The server evaluates the group's listener rules to + /// decide which, so this mode costs what a full read costs. + case fullOrRedacted // = 1 + + /// Redacted content, always. Every viewer who may read the chat at all — + /// in full or redacted — gets it redacted, with Message.redacted set on + /// every message; every other viewer is DENIED. A member asking for + /// REDACTED gets placeholders too. The server does not evaluate the + /// group's listener rules, only that it carries one, so this is the cheap + /// way to render a group blurred, and the mode to use whenever the client + /// intends to render it blurred regardless of whether the viewer could + /// read it in full. + case redacted // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .full + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .full + case 1: self = .fullOrRedacted + case 2: self = .redacted + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .full: return 0 + case .fullOrRedacted: return 1 + case .redacted: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Flipcash_Messaging_V1_ViewMode] = [ + .full, + .fullOrRedacted, + .redacted, + ] + +} + public struct Flipcash_Messaging_V1_MessageId: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -151,6 +229,37 @@ public struct Flipcash_Messaging_V1_Message: Sendable { /// Clears the value of `reactions`. Subsequent reads from it will return its default value. public mutating func clearReactions() {self._reactions = nil} + /// Set when this copy of the message was redacted for the viewer: the + /// viewer sees that the message exists and its shape, never what it says. + /// The content keeps its kind and structure but holds placeholders — text + /// of the same script, length and line structure; media with its + /// dimensions and blurhash but no download_url; a reply to the same + /// message with a placeholder body. Cash, system and deleted content are + /// not redacted. + /// + /// A copy is redacted for one of two reasons, and the client cannot tell + /// which from the message alone (see ViewMode on the read request): + /// - The viewer asked for FULL_OR_REDACTED and is a non-member of a group + /// whose listener rules they do not satisfy. Full content would have + /// been DENIED. + /// - The viewer asked for REDACTED, whatever their standing. + /// + /// Clients render a redacted message blurred, the way they render a + /// blurhash, and must not offer to copy, quote, download or otherwise + /// surface its content. The placeholder is a pure function of the + /// message's identity and shape, so it is stable across pages, devices and + /// view modes. A redacted copy is not a version of the message: + /// event_sequence still describes the underlying message, and a client + /// that later reads the same message unredacted replaces the placeholder + /// because the read was unredacted, not because of a higher + /// event_sequence. Clients should keep redacted and unredacted copies of a + /// chat apart (keyed by the ViewMode the read was made under) rather than + /// merging them into one history. + /// + /// Per-viewer, like EmojiReaction.self_reactor. Absent on every message + /// from a server that does not redact. + public var redacted: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -455,7 +564,8 @@ public struct Flipcash_Messaging_V1_Emoji: Sendable { public init() {} } -/// Reactor identifies a user who reacted to a message and when they did so. +/// Reactor identifies a user who reacted to a message with a given emoji, and +/// when they did so. public struct Flipcash_Messaging_V1_Reactor: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -470,7 +580,8 @@ public struct Flipcash_Messaging_V1_Reactor: Sendable { /// Clears the value of `userID`. Subsequent reads from it will return its default value. public mutating func clearUserID() {self._userID = nil} - /// Timestamp the user added this reaction. + /// Timestamp the user added this reaction. Display only: reactors are never + /// ordered by it (see version). public var reactedTs: SwiftProtobuf.Google_Protobuf_Timestamp { get {return _reactedTs ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_reactedTs = newValue} @@ -480,6 +591,15 @@ public struct Flipcash_Messaging_V1_Reactor: Sendable { /// Clears the value of `reactedTs`. Subsequent reads from it will return its default value. public mutating func clearReactedTs() {self._reactedTs = nil} + /// 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. + public var version: UInt64 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -538,14 +658,37 @@ public struct Flipcash_Messaging_V1_EmojiReaction: Sendable { /// returned here. public var count: UInt64 = 0 - /// Whether the requesting user reacted with this emoji. Per-viewer: count and - /// sample_reactors are shareable across users, but this bit is computed for - /// the caller. - public var reactedBySelf: Bool = false - - /// A small sample of reactors, with their reaction timestamps (e.g. for - /// rendering a few avatars), capped well below count. The complete, paged - /// reactor list is fetched on demand via GetReactors. + /// The requesting user's own Reactor entry for this emoji: set exactly when + /// they currently react with it, absent when they do not. Per-viewer: count + /// and sample_reactors are shareable across users, but this is computed for + /// the caller. It is the same entry the reactor list carries, whether or + /// not the viewer still sits in sample_reactors, so a client can render + /// itself among the reactors, or place itself in a partially loaded reactor + /// list by its version, without paging GetReactors to find its own row. + /// + /// Its presence answers "did I react"; its version is NOT the watermark for + /// that toggle. An EmojiReaction is a snapshot at `version`, and every + /// transition of the viewer's at or below it is already reflected here, so + /// live ReactionUpdates for the viewer are applied against `version` as for + /// any other actor. In an AddReactionResponse the entry's version equals + /// `version`. In a summary read it is normally at most `version`, but can + /// exceed it by one: the viewer's own add landing between the server's + /// aggregate read and its per-viewer read. Then this entry is the newer + /// truth and the aggregate is one transition behind, which the add's own + /// ReactionUpdate or the next refresh reconciles. + public var selfReactor: Flipcash_Messaging_V1_Reactor { + get {return _selfReactor ?? Flipcash_Messaging_V1_Reactor()} + set {_selfReactor = newValue} + } + /// Returns true if `selfReactor` has been explicitly set. + public var hasSelfReactor: Bool {return self._selfReactor != nil} + /// Clears the value of `selfReactor`. Subsequent reads from it will return its default value. + 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. public var sampleReactors: [Flipcash_Messaging_V1_Reactor] = [] /// Version of this emoji's aggregate on the message: assigned by the server @@ -553,12 +696,22 @@ public struct Flipcash_Messaging_V1_EmojiReaction: Sendable { /// /// Opaque to clients, and for ordering only. Apply reaction updates /// last-writer-wins by this value per (message, emoji) — and per actor for - /// reacted_by_self — and treat a loaded summary as stale when a higher + /// self_reactor — and treat a loaded summary as stale when a higher /// version arrives. It is compared the same way as chat.v1.RosterSummary. /// version. It is NOT the chat event sequence (reactions never advance /// that), and it is NOT gapless: a skipped value means nothing, and there is /// no delta to fetch against it — a missed update is reconciled by /// refreshing the summary on view. + /// + /// Keep the watermark after the emoji empties. The server retains the + /// version across an emoji's count reaching 0 and being re-added, so a + /// re-add always arrives with a higher version than the removal; a client + /// that forgets the (message, emoji) version when it stops rendering the + /// emoji has no way to reject a delayed, lower-versioned ADDED for it, and + /// would resurrect an emoji the server has already emptied. Hide the entry, + /// keep its version, at least for the session. A summary omits emptied + /// emoji entirely — their versions are not surfaced — so a refresh cannot + /// restore a watermark once forgotten. public var version: UInt64 = 0 public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -566,6 +719,7 @@ public struct Flipcash_Messaging_V1_EmojiReaction: Sendable { public init() {} fileprivate var _emoji: Flipcash_Messaging_V1_Emoji? = nil + fileprivate var _selfReactor: Flipcash_Messaging_V1_Reactor? = nil } /// ReactionUpdate is a best-effort, real-time reaction change for a single @@ -598,9 +752,10 @@ 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 renders - /// reacted_by_self by comparing this to itself, so a reaction made on the - /// user's other device is reflected. + /// 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. public var actor: Flipcash_Common_V1_UserId { get {return _actor ?? Flipcash_Common_V1_UserId()} set {_actor = newValue} @@ -613,19 +768,22 @@ public struct Flipcash_Messaging_V1_ReactionUpdate: Sendable { public var action: Flipcash_Messaging_V1_ReactionUpdate.Action = .unknown /// The emoji's total reactor count after this change. 0 means no reactors - /// remain and the client should drop the entry from the summary. + /// remain: stop rendering the emoji, but keep its version watermark (see + /// EmojiReaction.version) so a delayed, lower-versioned ADDED for it is + /// still ignored rather than resurrecting it. public var count: UInt64 = 0 /// The emoji aggregate's version after this change. Clients apply /// last-writer-wins by this value: ignore the count if version <= the count - /// watermark held, and ignore the actor's reacted_by_self toggle if + /// watermark held, and ignore the actor's self_reactor toggle if /// version <= the per-actor watermark held. Matches EmojiReaction.version. public var version: UInt64 = 0 /// When the actor reacted. On ADDED, clients record this as the actor's - /// Reactor.reacted_ts (e.g. when slotting them into sample_reactors); ignored - /// for REMOVED. This is a display timestamp, distinct from `sequence`, which - /// is the ordering key. + /// 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. public var reactedTs: SwiftProtobuf.Google_Protobuf_Timestamp { get {return _reactedTs ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_reactedTs = newValue} @@ -1035,6 +1193,10 @@ public struct Flipcash_Messaging_V1_IsTypingNotificationBatch: Sendable { fileprivate let _protobuf_package = "flipcash.messaging.v1" +extension Flipcash_Messaging_V1_ViewMode: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0FULL\0\u{1}FULL_OR_REDACTED\0\u{1}REDACTED\0") +} + extension Flipcash_Messaging_V1_MessageId: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".MessageId" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}value\0") @@ -1097,7 +1259,7 @@ extension Flipcash_Messaging_V1_ClientMessageId: SwiftProtobuf.Message, SwiftPro extension Flipcash_Messaging_V1_Message: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Message" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}message_id\0\u{3}sender_id\0\u{1}content\0\u{1}ts\0\u{3}unread_seq\0\u{3}last_edited_ts\0\u{3}event_sequence\0\u{1}reactions\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}message_id\0\u{3}sender_id\0\u{1}content\0\u{1}ts\0\u{3}unread_seq\0\u{3}last_edited_ts\0\u{3}event_sequence\0\u{1}reactions\0\u{1}redacted\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1113,6 +1275,7 @@ extension Flipcash_Messaging_V1_Message: SwiftProtobuf.Message, SwiftProtobuf._M case 6: try { try decoder.decodeSingularMessageField(value: &self._lastEditedTs) }() case 7: try { try decoder.decodeSingularUInt64Field(value: &self.eventSequence) }() case 8: try { try decoder.decodeSingularMessageField(value: &self._reactions) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self.redacted) }() default: break } } @@ -1147,6 +1310,9 @@ extension Flipcash_Messaging_V1_Message: SwiftProtobuf.Message, SwiftProtobuf._M try { if let v = self._reactions { try visitor.visitSingularMessageField(value: v, fieldNumber: 8) } }() + if self.redacted != false { + try visitor.visitSingularBoolField(value: self.redacted, fieldNumber: 9) + } try unknownFields.traverse(visitor: &visitor) } @@ -1159,6 +1325,7 @@ extension Flipcash_Messaging_V1_Message: SwiftProtobuf.Message, SwiftProtobuf._M if lhs._lastEditedTs != rhs._lastEditedTs {return false} if lhs.eventSequence != rhs.eventSequence {return false} if lhs._reactions != rhs._reactions {return false} + if lhs.redacted != rhs.redacted {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1556,7 +1723,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") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}reacted_ts\0\u{1}version\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1566,6 +1733,7 @@ extension Flipcash_Messaging_V1_Reactor: SwiftProtobuf.Message, SwiftProtobuf._M switch fieldNumber { 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) }() default: break } } @@ -1582,12 +1750,16 @@ extension Flipcash_Messaging_V1_Reactor: SwiftProtobuf.Message, SwiftProtobuf._M try { if let v = self._reactedTs { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() + if self.version != 0 { + try visitor.visitSingularUInt64Field(value: self.version, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Messaging_V1_Reactor, rhs: Flipcash_Messaging_V1_Reactor) -> Bool { if lhs._userID != rhs._userID {return false} if lhs._reactedTs != rhs._reactedTs {return false} + if lhs.version != rhs.version {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1634,7 +1806,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}reacted_by_self\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") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1644,7 +1816,7 @@ extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProto switch fieldNumber { case 1: try { try decoder.decodeSingularMessageField(value: &self._emoji) }() case 2: try { try decoder.decodeSingularUInt64Field(value: &self.count) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.reactedBySelf) }() + 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) }() default: break @@ -1663,9 +1835,9 @@ extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProto if self.count != 0 { try visitor.visitSingularUInt64Field(value: self.count, fieldNumber: 2) } - if self.reactedBySelf != false { - try visitor.visitSingularBoolField(value: self.reactedBySelf, fieldNumber: 3) - } + try { if let v = self._selfReactor { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() if !self.sampleReactors.isEmpty { try visitor.visitRepeatedMessageField(value: self.sampleReactors, fieldNumber: 4) } @@ -1678,7 +1850,7 @@ extension Flipcash_Messaging_V1_EmojiReaction: SwiftProtobuf.Message, SwiftProto public static func ==(lhs: Flipcash_Messaging_V1_EmojiReaction, rhs: Flipcash_Messaging_V1_EmojiReaction) -> Bool { if lhs._emoji != rhs._emoji {return false} if lhs.count != rhs.count {return false} - if lhs.reactedBySelf != rhs.reactedBySelf {return false} + if lhs._selfReactor != rhs._selfReactor {return false} if lhs.sampleReactors != rhs.sampleReactors {return false} if lhs.version != rhs.version {return false} if lhs.unknownFields != rhs.unknownFields {return false} diff --git a/Sources/Flipcash2ClientProtocol/push_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/push_v1_model.pb.swift index cd67745..db38832 100644 --- a/Sources/Flipcash2ClientProtocol/push_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/push_v1_model.pb.swift @@ -253,6 +253,11 @@ public struct Flipcash_Push_V1_ChatMetadata: Sendable { /// Clears the value of `message`. Subsequent reads from it will return its default value. public mutating func clearMessage() {self._message = nil} + /// Whether the recipient had this chat muted when the push was sent. + /// The push is still delivered so the client can store the message, + /// but the client must not present a notification for it. + public var muted: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -464,7 +469,7 @@ extension Flipcash_Push_V1_Navigation: SwiftProtobuf.Message, SwiftProtobuf._Mes extension Flipcash_Push_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}sending_user_id\0\u{1}type\0\u{1}message\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sending_user_id\0\u{1}type\0\u{1}message\0\u{1}muted\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -475,6 +480,7 @@ extension Flipcash_Push_V1_ChatMetadata: SwiftProtobuf.Message, SwiftProtobuf._M case 1: try { try decoder.decodeSingularMessageField(value: &self._sendingUserID) }() case 2: try { try decoder.decodeSingularEnumField(value: &self.type) }() case 3: try { try decoder.decodeSingularMessageField(value: &self._message) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.muted) }() default: break } } @@ -494,6 +500,9 @@ extension Flipcash_Push_V1_ChatMetadata: SwiftProtobuf.Message, SwiftProtobuf._M try { if let v = self._message { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() + if self.muted != false { + try visitor.visitSingularBoolField(value: self.muted, fieldNumber: 4) + } try unknownFields.traverse(visitor: &visitor) } @@ -501,6 +510,7 @@ extension Flipcash_Push_V1_ChatMetadata: SwiftProtobuf.Message, SwiftProtobuf._M if lhs._sendingUserID != rhs._sendingUserID {return false} if lhs.type != rhs.type {return false} if lhs._message != rhs._message {return false} + if lhs.muted != rhs.muted {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.grpc.swift b/Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.grpc.swift new file mode 100644 index 0000000..bb007b3 --- /dev/null +++ b/Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.grpc.swift @@ -0,0 +1,221 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: reporting/v1/reporting_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.reporting.v1.Reporting + +/// Namespace containing generated types for the "flipcash.reporting.v1.Reporting" service. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public enum Flipcash_Reporting_V1_Reporting { + /// Service descriptor for the "flipcash.reporting.v1.Reporting" service. + public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.reporting.v1.Reporting") + /// Namespace for method metadata. + public enum Method { + /// Namespace for "Report" metadata. + public enum Report { + /// Request type for "Report". + public typealias Input = Flipcash_Reporting_V1_ReportRequest + /// Response type for "Report". + public typealias Output = Flipcash_Reporting_V1_ReportResponse + /// Descriptor for "Report". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.reporting.v1.Reporting"), + method: "Report" + ) + } + /// Descriptors for all methods in the "flipcash.reporting.v1.Reporting" service. + public static let descriptors: [GRPCCore.MethodDescriptor] = [ + Report.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.reporting.v1.Reporting" service. + public static let flipcash_reporting_v1_Reporting = GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.reporting.v1.Reporting") +} + +// MARK: flipcash.reporting.v1.Reporting (client) + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Flipcash_Reporting_V1_Reporting { + /// Generated client protocol for the "flipcash.reporting.v1.Reporting" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > Reporting lets users flag content or other users for review. + public protocol ClientProtocol: Sendable { + /// Call the "Report" method. + /// + /// > Source IDL Documentation: + /// > + /// > Report files a report against a user, chat, message, or blob. Reports + /// > are advisory: filing one has no immediate client-visible effect and + /// > outcomes are not surfaced back to the reporter. Reporting the same + /// > target more than once is a no-op and returns OK. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Reporting_V1_ReportRequest` message. + /// - serializer: A serializer for `Flipcash_Reporting_V1_ReportRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Reporting_V1_ReportResponse` 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 report( + 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.reporting.v1.Reporting" 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: + /// > + /// > Reporting lets users flag content or other users for review. + 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 "Report" method. + /// + /// > Source IDL Documentation: + /// > + /// > Report files a report against a user, chat, message, or blob. Reports + /// > are advisory: filing one has no immediate client-visible effect and + /// > outcomes are not surfaced back to the reporter. Reporting the same + /// > target more than once is a no-op and returns OK. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Reporting_V1_ReportRequest` message. + /// - serializer: A serializer for `Flipcash_Reporting_V1_ReportRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Reporting_V1_ReportResponse` 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 report( + 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_Reporting_V1_Reporting.Method.Report.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_Reporting_V1_Reporting.ClientProtocol { + /// Call the "Report" method. + /// + /// > Source IDL Documentation: + /// > + /// > Report files a report against a user, chat, message, or blob. Reports + /// > are advisory: filing one has no immediate client-visible effect and + /// > outcomes are not surfaced back to the reporter. Reporting the same + /// > target more than once is a no-op and returns OK. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Reporting_V1_ReportRequest` 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 report( + 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.report( + 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_Reporting_V1_Reporting.ClientProtocol { + /// Call the "Report" method. + /// + /// > Source IDL Documentation: + /// > + /// > Report files a report against a user, chat, message, or blob. Reports + /// > are advisory: filing one has no immediate client-visible effect and + /// > outcomes are not surfaced back to the reporter. Reporting the same + /// > target more than once is a no-op and returns OK. + /// + /// - 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 report( + _ message: Flipcash_Reporting_V1_ReportRequest, + 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.report( + request: request, + options: options, + onResponse: handleResponse + ) + } +} \ No newline at end of file diff --git a/Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.pb.swift b/Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.pb.swift new file mode 100644 index 0000000..2a2705f --- /dev/null +++ b/Sources/Flipcash2ClientProtocol/reporting_v1_reporting_service.pb.swift @@ -0,0 +1,369 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: reporting/v1/reporting_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_Reporting_V1_ReportRequest: 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 target being reported + public var target: Flipcash_Reporting_V1_ReportRequest.OneOf_Target? = nil + + /// A user + public var userID: Flipcash_Common_V1_UserId { + get { + if case .userID(let v)? = target {return v} + return Flipcash_Common_V1_UserId() + } + set {target = .userID(newValue)} + } + + /// A chat + public var chatID: Flipcash_Common_V1_ChatId { + get { + if case .chatID(let v)? = target {return v} + return Flipcash_Common_V1_ChatId() + } + set {target = .chatID(newValue)} + } + + /// A message within a chat + public var message: Flipcash_Reporting_V1_ReportRequest.ReportedMessage { + get { + if case .message(let v)? = target {return v} + return Flipcash_Reporting_V1_ReportRequest.ReportedMessage() + } + set {target = .message(newValue)} + } + + /// A blob + public var blobID: Flipcash_Blob_V1_BlobId { + get { + if case .blobID(let v)? = target {return v} + return Flipcash_Blob_V1_BlobId() + } + set {target = .blobID(newValue)} + } + + /// Optional free-form text from the reporter describing the reason for + /// the report. Not moderated or shown to other users. + public var description_p: String = String() + + 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 target being reported + public enum OneOf_Target: Equatable, Sendable { + /// A user + case userID(Flipcash_Common_V1_UserId) + /// A chat + case chatID(Flipcash_Common_V1_ChatId) + /// A message within a chat + case message(Flipcash_Reporting_V1_ReportRequest.ReportedMessage) + /// A blob + case blobID(Flipcash_Blob_V1_BlobId) + + } + + /// ReportedMessage identifies a message. MessageId is a per-chat sequence + /// number, so the chat it belongs to must be provided alongside it. + public struct ReportedMessage: 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 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} + + 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} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil + fileprivate var _messageID: Flipcash_Messaging_V1_MessageId? = nil + } + + public init() {} + + fileprivate var _auth: Flipcash_Common_V1_Auth? = nil +} + +public struct Flipcash_Reporting_V1_ReportResponse: 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_Reporting_V1_ReportResponse.Result = .ok + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum Result: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case ok // = 0 + case denied // = 1 + case notFound // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .ok + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .denied + case 2: self = .notFound + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .ok: return 0 + case .denied: return 1 + case .notFound: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Flipcash_Reporting_V1_ReportResponse.Result] = [ + .ok, + .denied, + .notFound, + ] + + } + + public init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "flipcash.reporting.v1" + +extension Flipcash_Reporting_V1_ReportRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ReportRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}chat_id\0\u{1}message\0\u{3}blob_id\0\u{1}description\0\u{2}\u{5}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_UserId? + var hadOneofValue = false + if let current = self.target { + hadOneofValue = true + if case .userID(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.target = .userID(v) + } + }() + case 2: try { + var v: Flipcash_Common_V1_ChatId? + var hadOneofValue = false + if let current = self.target { + hadOneofValue = true + if case .chatID(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.target = .chatID(v) + } + }() + case 3: try { + var v: Flipcash_Reporting_V1_ReportRequest.ReportedMessage? + var hadOneofValue = false + if let current = self.target { + hadOneofValue = true + if case .message(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.target = .message(v) + } + }() + case 4: try { + var v: Flipcash_Blob_V1_BlobId? + var hadOneofValue = false + if let current = self.target { + hadOneofValue = true + if case .blobID(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.target = .blobID(v) + } + }() + case 5: try { try decoder.decodeSingularStringField(value: &self.description_p) }() + 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 + switch self.target { + case .userID?: try { + guard case .userID(let v)? = self.target else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .chatID?: try { + guard case .chatID(let v)? = self.target else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .message?: try { + guard case .message(let v)? = self.target else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .blobID?: try { + guard case .blobID(let v)? = self.target else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } + if !self.description_p.isEmpty { + try visitor.visitSingularStringField(value: self.description_p, fieldNumber: 5) + } + try { if let v = self._auth { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Reporting_V1_ReportRequest, rhs: Flipcash_Reporting_V1_ReportRequest) -> Bool { + if lhs.target != rhs.target {return false} + if lhs.description_p != rhs.description_p {return false} + if lhs._auth != rhs._auth {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Reporting_V1_ReportRequest.ReportedMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Reporting_V1_ReportRequest.protoMessageName + ".ReportedMessage" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}message_id\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) }() + 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 unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Reporting_V1_ReportRequest.ReportedMessage, rhs: Flipcash_Reporting_V1_ReportRequest.ReportedMessage) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs._messageID != rhs._messageID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Reporting_V1_ReportResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ReportResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\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) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.result != .ok { + try visitor.visitSingularEnumField(value: self.result, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Reporting_V1_ReportResponse, rhs: Flipcash_Reporting_V1_ReportResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Reporting_V1_ReportResponse.Result: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}NOT_FOUND\0") +} diff --git a/flipcash2.lock b/flipcash2.lock index d58056d..ac4a823 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: e1f4116c499718401d002cf79ce64229149ee297 -subject: Add idempotency key to StartChat RPC request (#102) +commit: dd5e92db6f76700ab6b565f4eff1f9fb25140c9a +subject: Add Reporting RPC service (#113) diff --git a/proto/blob/v1/model.proto b/proto/blob/v1/model.proto index ff57f9a..98de2c8 100644 --- a/proto/blob/v1/model.proto +++ b/proto/blob/v1/model.proto @@ -76,7 +76,14 @@ message BlobMetadata { // Ephemeral, server-minted URL for fetching the blob bytes, together with // the instant it expires. Re-issued on every fetch — see DownloadUrl. - DownloadUrl download_url = 3 [(validate.rules).message.required = true]; + // + // Absent when the viewer may know what the blob is but may not fetch its + // bytes: a redacted message (see messaging.v1.Message.redacted) carries + // each rendition's intrinsic metadata — for an image, its dimensions and + // blurhash — without a URL. A client renders such media from the blurhash + // alone and does not call GetBlobs to mint one; the blob is not readable + // by that viewer and the call would not return it. + DownloadUrl download_url = 3; // Kind-specific metadata the server derived from the bytes. Exactly one // variant is set for a recognized media kind; left unset for opaque blobs. diff --git a/proto/chat/v1/chat_service.proto b/proto/chat/v1/chat_service.proto index 69dab53..b4cfd2b 100644 --- a/proto/chat/v1/chat_service.proto +++ b/proto/chat/v1/chat_service.proto @@ -9,6 +9,7 @@ option objc_class_prefix = "FPBChatV1"; import "blob/v1/model.proto"; import "chat/v1/model.proto"; import "common/v1/common.proto"; +import "messaging/v1/model.proto"; import "moderation/v1/model.proto"; import "validate/validate.proto"; @@ -72,11 +73,39 @@ service Chat { // LeaveChat removes the caller from a chat's roster. rpc LeaveChat(LeaveChatRequest) returns (LeaveChatResponse); + + // MuteChat mutes a chat for the caller, until a time or indefinitely. + // + // Only a member may mute. Muting does not affect the event stream or + // message delivery: pushes for a muted chat are still sent, flagged with + // push.v1.ChatMetadata.muted, and the client suppresses the notification. + // Calling it again with a different duration replaces the mute. An + // identical request is a no-op. A mute is cleared when the caller leaves + // the chat. Every real change reaches the caller's other devices as a + // MetadataUpdate.ViewerStateChanged on the event stream. + rpc MuteChat(MuteChatRequest) returns (MuteChatResponse); + + // UnmuteChat clears the caller's mute on a chat. Unmuting a chat that is + // not muted is a no-op. Every real change reaches the caller's other devices + // as a MetadataUpdate.ViewerStateChanged on the event stream. + rpc UnmuteChat(UnmuteChatRequest) returns (UnmuteChatResponse); } message GetChatRequest { common.v1.ChatId chat_id = 1; + // What the client intends to render of the chat's messaging state — its + // last_message and latest_event_sequence — and so whether they may be + // returned redacted (see messaging.v1.ViewMode). A group's record (title, + // picture, rules, roster summary) is returned to every registered user + // whatever the mode; the mode decides only the messaging state, which is + // withheld from a viewer who may not read the chat under it. Unset (FULL) + // is the pre-redaction contract: messaging state for a viewer who may + // read the chat in full, the bare record for anyone else. + messaging.v1.ViewMode view_mode = 2 [(validate.rules).enum = { + in: [0, 1, 2] // FULL, FULL_OR_REDACTED, REDACTED + }]; + common.v1.Auth auth = 10; } @@ -278,3 +307,44 @@ message LeaveChatResponse { NOT_FOUND = 2; } } + +message MuteChatRequest { + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + // How long to mute for. A timed mute must end in the future; the server + // rejects one that does not. + MuteState mute = 2 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message MuteChatResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; // Caller is not a member + NOT_FOUND = 2; + } + + // The caller's state for the chat after the write, so the client can + // apply it at its version. Set only when result == OK. + ViewerState viewer_state = 2; +} + +message UnmuteChatRequest { + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message UnmuteChatResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + NOT_FOUND = 2; + } + + // As for MuteChatResponse.viewer_state. + ViewerState viewer_state = 2; +} diff --git a/proto/chat/v1/model.proto b/proto/chat/v1/model.proto index b8c4068..c13eb3e 100644 --- a/proto/chat/v1/model.proto +++ b/proto/chat/v1/model.proto @@ -34,7 +34,10 @@ message Metadata { // RosterSummary.member_count to infer if there are more members. repeated Member members = 3; - // The last message in this chat + // The last message in this chat. Withheld, along with + // latest_event_sequence, from a viewer who may not read the chat. May be + // redacted (see messaging.v1.Message.redacted) when the read was made + // under a messaging.v1.ViewMode other than FULL. messaging.v1.Message last_message = 4; // The timestamp of the last activity in this chat @@ -71,6 +74,9 @@ message Metadata { // Rules governing participation in this chat. Only supported for group // chats. If not set, the chat has no participation requirements. Rules rules = 11; + + // Per-viewer chat state, absent when the chat holds nothing about them. + ViewerState viewer_state = 12; } // Rules define the requirements a user must satisfy to participate in a chat. @@ -171,7 +177,9 @@ message RosterSummary { // Version of the roster: advanced by exactly one on every change to the // membership records — a member joining, a member leaving, and in future // any change to what the chat records about a member (e.g. a role) — and - // never by an idempotent no-op or a profile change. + // never by an idempotent no-op or a profile change. A viewer's own state + // (see ViewerState) is not part of the roster: it is private to that + // viewer, carries its own version, and never moves this one. // // Opaque to clients. Compare it against the last value seen: a different // value means the cached member list may be stale and should be refetched. @@ -188,6 +196,7 @@ message MetadataUpdate { FullRefresh full_refresh = 1; LastActivityChanged last_activity_changed = 2; + ViewerStateChanged viewer_state_changed = 3; } // Refreshes the entire chat metadata @@ -199,6 +208,11 @@ message MetadataUpdate { message LastActivityChanged { google.protobuf.Timestamp new_last_activity = 1 [(validate.rules).timestamp.required = true]; } + + // The receiving user's own state for the chat has changed + message ViewerStateChanged { + ViewerState viewer_state = 1 [(validate.rules).message.required = true]; + } } // RosterUpdate is a best-effort, real-time change to a chat's roster — a @@ -260,6 +274,42 @@ message RosterUpdateBatch { }]; } +// ViewerState is what this chat holds about the requesting user, +// independent of whether they are a member. It is per-viewer and never +// shared with other members. +message ViewerState { + // State the viewer set for themselves through their own RPCs. + Settings settings = 1; + + // Advanced by exactly one on every real change to any field of this + // message, never by a no-op. Compared like RosterSummary.version: apply + // the greater value and drop the rest, so delivery order does not matter. + // It is not the chat event sequence and has no delta. + uint64 version = 10; + + message Settings { + // Present if and only if the chat is muted for the viewer. Muted pushes + // are still delivered, flagged with push.v1.ChatMetadata.muted, and the + // client suppresses the notification. + MuteState mute = 1; + } +} + +message MuteState { + oneof duration { + option (validate.required) = true; + + // Muted until this time, after which the mute lapses. Nothing is + // sent when it does, so the client owns the countdown. + google.protobuf.Timestamp until = 1; + + // Muted forever until explicitly unmuted. + Forever forever = 2; + } + + message Forever {} +} + // IdempotencyKey lets a client safely retry a request that creates something. // The server derives the created object's identity from the caller and this // key, so a retry with the same key returns the original result instead of diff --git a/proto/event/v1/event_streaming_service.proto b/proto/event/v1/event_streaming_service.proto index b25226b..7276589 100644 --- a/proto/event/v1/event_streaming_service.proto +++ b/proto/event/v1/event_streaming_service.proto @@ -8,11 +8,29 @@ option objc_class_prefix = "FPBEventV1"; import "event/v1/model.proto"; import "common/v1/common.proto"; +import "messaging/v1/model.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; service EventStreaming { - // StreamEvents streams events for the requesting user. + // StreamEvents streams events to the requesting user. Params, the first + // message on the stream, selects what is streamed: + // + // - With no target set, every event addressed to the signing user, across + // all of their events. This stream lives until either side closes it. + // - With chat_preview set, ChatUpdate for that single group chat, under + // a messaging.v1.ViewMode, and nothing else, for a bounded window of + // time. This is how a viewer who is not a member of a group — and so is + // not addressed by its events — previews it live before joining, and + // how any viewer previews a group redacted. It is the only + // chat-targeted stream; there is no open-ended stream for a single + // chat, and no stream of any kind targeted at a DM. + // + // The two are independent. A client may hold several streams at once, e.g. + // a user stream plus a preview stream for the group it is currently + // previewing as a non-member. Nothing is deduplicated across streams: a + // member who opens a preview stream for one of their own chats receives + // that chat's updates on both. rpc StreamEvents(stream StreamEventsRequest) returns (stream StreamEventsResponse); // ForwardEvents is an internal RPC for forwarding events to another server. @@ -35,6 +53,77 @@ message StreamEventsRequest { // It is used primarily as a nonce for auth. Server may reject // timestamps that are too far in the future or past. google.protobuf.Timestamp ts = 2 [(validate.rules).timestamp.required = true]; + + // What the stream is for. Optional: when nothing is set, the stream is + // for the signing user and carries every event addressed to them, the + // contract that predates this field. When set, it takes over, and the + // stream carries only what the target describes. + oneof target { + ChatPreviewParams chat_preview = 3; + } + } + + // ChatPreviewParams targets a stream at a preview of a single group chat: + // ChatUpdate for chat_id, under view_mode, and nothing else, for a bounded + // window of time. No update for any other chat, is ever delivered on it. + // + // Only group chats can be previewed. A DM has no one to preview it — a + // non-member of a DM may not read it under any mode — so a chat_id naming + // a DM is DENIED, whatever the viewer's standing and whatever the mode, + // by the same rule that denies every other read of it. A DM's members + // follow it on their user stream. + // + // The stream is time-bounded. The server fixes the lifetime of a preview + // at open — the bound is the server's, not the client's, and there is no + // way to request a longer one — and ends the stream with STREAM_EXPIRED + // when it elapses, whether or not anything was delivered. Pings and pongs + // keep the stream healthy, not alive: a pong does not extend the window. + // A client still previewing the chat when the window closes opens a new + // preview stream, and reconciles anything it missed in between the way it + // would after any reconnect (GetDelta for the event log, a refresh of the + // overlays). The server may refuse to open a preview, with DENIED, to a + // viewer who opens too many in too short a time. + // + // A preview is for a viewer who is not (yet) a member of the chat. A + // member's own chats are already carried on their user stream, which is + // not time-bounded, and a client should not preview a chat it is a member + // of; the server does not refuse it, but delivers the chat's updates on + // both streams. A viewer who joins the chat while previewing it switches + // to their user stream: the preview keeps delivering until its window + // closes, but is not extended by the join. + // + // The stream opens only if the viewer may read the chat under the mode, + // by the same rule as any other read under a messaging.v1.ViewMode: a + // member, or a non-member of a group who satisfies its listener rules, + // may read in full; a non-member of a group with listener rules they do + // not satisfy may read redacted; anyone else is DENIED. A chat that does + // not exist is NOT_FOUND. Standing is evaluated at stream open, and the + // server may end the stream with DENIED before the window closes if the + // viewer later may not read the chat under the mode — e.g. the rules + // changed, or they were removed from the roster. + // + // Within a ChatUpdate, the mode decides only the messages: those carried + // by events, and last_message in metadata_updates. Every message on the + // stream is delivered under the mode fixed at open, like GetDelta, never + // mixed, with Message.redacted set on each when redacted. Pointer, + // typing, reaction and roster overlays are delivered whatever the mode. + // + // A client previewing a chat redacted keeps this stream and its + // GetMessages/GetDelta reads under the same mode, so the events it + // applies are shaped like the state it applies them to (see + // messaging.v1.Message.redacted). + message ChatPreviewParams { + // The one chat whose updates are streamed. Must be a group chat; a DM + // is DENIED. + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + // What the client intends to render of the chat, and so whether its + // messages may be delivered redacted (see messaging.v1.ViewMode). + // Unset (FULL) delivers full content to a viewer who may read the + // chat in full, and is DENIED to everyone else. + messaging.v1.ViewMode view_mode = 2 [(validate.rules).enum = { + in: [0, 1, 2] // FULL, FULL_OR_REDACTED, REDACTED + }]; } } @@ -52,6 +141,8 @@ message StreamEventsResponse { enum Code { DENIED = 0; INVALID_TIMESTAMP = 1; + NOT_FOUND = 2; + STREAM_EXPIRED = 3; } } } diff --git a/proto/event/v1/model.proto b/proto/event/v1/model.proto index 2074d97..5a2d252 100644 --- a/proto/event/v1/model.proto +++ b/proto/event/v1/model.proto @@ -125,7 +125,7 @@ message ChatUpdate { // If present, best-effort real-time reaction changes for messages in the // chat. Like pointer_updates, reactions are a convergent overlay — NOT part // of the gap-detected event log; clients apply them last-writer-wins by - // ReactionUpdate.sequence and reconcile any misses by refreshing a message's + // ReactionUpdate.version and reconcile any misses by refreshing a message's // ReactionSummary on view. messaging.v1.ReactionUpdateBatch reaction_updates = 7; diff --git a/proto/messaging/v1/messaging_service.proto b/proto/messaging/v1/messaging_service.proto index c392a8d..3b99992 100644 --- a/proto/messaging/v1/messaging_service.proto +++ b/proto/messaging/v1/messaging_service.proto @@ -74,14 +74,27 @@ service Messaging { // GetReactors returns the paged list of users who reacted to a message with // a given emoji — the on-demand drill-down behind EmojiReaction.count, which // never inlines the full reactor list. + // + // The reaction reads (GetReactors, GetReactionSummary, GetReactionSummaries) + // carry no ViewMode, since they return no Message, and are answered for + // anyone who may read the chat under any mode: a member, or a non-member of + // a group that carries a listener rule, whether or not they satisfy it. + // Reactions are an overlay — who reacted, with what, on which message is + // the conversation's movement, not its words — and are delivered whole to a + // redacted reader, on these reads as on a chat preview stream (see + // event.v1.StreamEventsRequest.ChatPreviewParams). A non-member's + // self_reactor is always absent: reacting is a member's write. Anyone who + // may not read the chat at all is DENIED. rpc GetReactors(GetReactorsRequest) returns (GetReactorsResponse); // GetReactionSummary fetches the current aggregate reaction state for a - // single message. + // single message. Answered for anyone who may read the chat under any mode + // (see GetReactors). rpc GetReactionSummary(GetReactionSummaryRequest) returns (GetReactionSummaryResponse); // GetReactionSummaries fetches the current aggregate reaction state using - // paged and batched APIs + // paged and batched APIs. Answered for anyone who may read the chat under + // any mode (see GetReactors). rpc GetReactionSummaries(GetReactionSummariesRequest) returns (GetReactionSummariesResponse); // AdvancePointer advances a pointer in message history for a chat member. @@ -98,6 +111,13 @@ message GetMessageRequest { MessageId message_id = 2 [(validate.rules).message.required = true]; + // What the client intends to render, and so whether the message may be + // returned redacted (see ViewMode). Unset (FULL) is the pre-redaction + // contract: full content, or DENIED. + ViewMode view_mode = 3 [(validate.rules).enum = { + in: [0, 1, 2] // FULL, FULL_OR_REDACTED, REDACTED + }]; + common.v1.Auth auth = 10; } @@ -122,6 +142,15 @@ message GetMessagesRequest { MessageIdBatch message_ids = 3; } + // What the client intends to render, and so whether the messages may be + // returned redacted (see ViewMode). Applies to both query forms. Unset + // (FULL) is the pre-redaction contract: full content, or DENIED. Every + // message in a response is returned under the same mode: a page is + // either wholly full or wholly redacted, never mixed. + ViewMode view_mode = 4 [(validate.rules).enum = { + in: [0, 1, 2] // FULL, FULL_OR_REDACTED, REDACTED + }]; + common.v1.Auth auth = 10; } @@ -145,6 +174,17 @@ message GetDeltaRequest { // the retained log. uint64 after_sequence = 2; + // What the client intends to render, and so whether the messages may be + // returned redacted (see ViewMode). Unset (FULL) is the pre-redaction + // contract: full content, or DENIED. The mode is fixed at stream open, + // like latest_sequence: every batch in the stream is returned under the + // same mode, never mixed. A client catching up a redacted view must use + // the same mode it read the history under, so the delta it applies is + // shaped like the state it applies it to. + ViewMode view_mode = 3 [(validate.rules).enum = { + in: [0, 1, 2] // FULL, FULL_OR_REDACTED, REDACTED + }]; + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; } @@ -322,7 +362,8 @@ message AddReactionResponse { TOO_MANY_REACTION_TYPES = 4; } - // The affected emoji's aggregate after the add (count, reacted_by_self true). + // The affected emoji's aggregate after the add (count, self_reactor set at + // the aggregate's version). EmojiReaction reaction = 2; } @@ -357,11 +398,13 @@ message GetReactorsRequest { // The emoji whose reactors to list. Emoji emoji = 3 [(validate.rules).message.required = true]; - // Paging over the reactor list (server-ordered, typically most-recent - // first). 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. + // Paging over the reactor list. The order is fixed: 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. common.v1.QueryOptions options = 4; common.v1.Auth auth = 10 [(validate.rules).message.required = true]; @@ -375,8 +418,9 @@ message GetReactorsResponse { MESSAGE_NOT_FOUND = 2; } - // A page of users who reacted with the requested emoji, with their reaction - // timestamps. Empty when the message exists but has no reactors for the emoji. + // 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. repeated Reactor reactors = 2 [(validate.rules).repeated = { max_items: 100 }]; @@ -391,6 +435,16 @@ message GetReactorsResponse { // the reactor list has been fully read. When true, the client should issue // another GetReactorsRequest with the returned paging_token. bool has_more = 4; + + // The emoji aggregate's version (EmojiReaction.version) the page is current + // to: the server reads it BEFORE the page, so the page reflects every add + // and remove up to this version and possibly some after it. A client + // 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. + uint64 version = 5; } message GetReactionSummaryRequest { @@ -409,9 +463,9 @@ message GetReactionSummaryResponse { MESSAGE_NOT_FOUND = 2; } - // The aggregate reaction state for the message. reacted_by_self is computed + // The aggregate reaction state for the message. self_reactor is computed // for the caller; clients still apply per (message, emoji) by - // EmojiReaction.sequence, so a summary that is slightly behind a live update + // EmojiReaction.version, so a summary that is slightly behind a live update // is harmlessly ignored rather than regressing state. ReactionSummary summary = 2; } @@ -437,9 +491,9 @@ message GetReactionSummariesResponse { } // One summary per requested message, keyed by ReactionSummary.message_id. - // reacted_by_self in each summary is computed for the caller; clients still - // apply per (message, emoji) by EmojiReaction.sequence, so a summary that - // is slightly behind a live update is harmlessly ignored rather than regressing + // self_reactor in each summary is computed for the caller; clients still + // apply per (message, emoji) by EmojiReaction.version, so a summary that is + // slightly behind a live update is harmlessly ignored rather than regressing // state. repeated ReactionSummary summaries = 2 [(validate.rules).repeated = { max_items: 100 diff --git a/proto/messaging/v1/model.proto b/proto/messaging/v1/model.proto index ddaada1..d104d1b 100644 --- a/proto/messaging/v1/model.proto +++ b/proto/messaging/v1/model.proto @@ -91,6 +91,82 @@ message Message { // clients refresh it on view and via live reaction updates rather than // through the event log. ReactionSummary reactions = 8; + + // Set when this copy of the message was redacted for the viewer: the + // viewer sees that the message exists and its shape, never what it says. + // The content keeps its kind and structure but holds placeholders — text + // of the same script, length and line structure; media with its + // dimensions and blurhash but no download_url; a reply to the same + // message with a placeholder body. Cash, system and deleted content are + // not redacted. + // + // A copy is redacted for one of two reasons, and the client cannot tell + // which from the message alone (see ViewMode on the read request): + // - The viewer asked for FULL_OR_REDACTED and is a non-member of a group + // whose listener rules they do not satisfy. Full content would have + // been DENIED. + // - The viewer asked for REDACTED, whatever their standing. + // + // Clients render a redacted message blurred, the way they render a + // blurhash, and must not offer to copy, quote, download or otherwise + // surface its content. The placeholder is a pure function of the + // message's identity and shape, so it is stable across pages, devices and + // view modes. A redacted copy is not a version of the message: + // event_sequence still describes the underlying message, and a client + // that later reads the same message unredacted replaces the placeholder + // because the read was unredacted, not because of a higher + // event_sequence. Clients should keep redacted and unredacted copies of a + // chat apart (keyed by the ViewMode the read was made under) rather than + // merging them into one history. + // + // Per-viewer, like EmojiReaction.self_reactor. Absent on every message + // from a server that does not redact. + bool redacted = 9; +} + +// ViewMode is what a client intends to render from a read of a chat's +// messages, and so how much of them the server may return. It is set on +// every read that returns a Message (GetMessage, GetMessages, GetDelta, +// chat.v1.GetChat, for last_message, and event.v1.StreamEvents when +// previewing a chat) and is combined with the viewer's standing in the chat: +// +// - A member, or a non-member of a group who satisfies its listener rules, +// may read the chat in full. +// - A non-member of a group who does not satisfy its listener rules may +// read it redacted, provided the group has at least one listener rule. +// - Anyone else — a non-member of a DM, or of a group with no listener +// rules — may not read it at all. +// +// The mode never widens what the standing allows: it can turn a full read +// into a redacted one, or a refusal into a redacted read, but never a +// redacted read into a full one. Redaction is the server's, not the +// client's; blurring full content on the device is not a substitute, since +// the content is still on the device. +// +// The default, FULL, is the contract that predates redaction, so a client +// that does not set the field behaves exactly as before. +enum ViewMode { + // Full content or nothing. A viewer who may read the chat in full gets + // it; every other viewer is DENIED. Never returns a redacted message, so + // a client that does not understand Message.redacted is never handed one. + FULL = 0; + + // The most the viewer's standing allows. A viewer who may read the chat + // in full gets it; a viewer who may only read it redacted gets it + // redacted, with Message.redacted set on every message; every other + // viewer is DENIED. The server evaluates the group's listener rules to + // decide which, so this mode costs what a full read costs. + FULL_OR_REDACTED = 1; + + // Redacted content, always. Every viewer who may read the chat at all — + // in full or redacted — gets it redacted, with Message.redacted set on + // every message; every other viewer is DENIED. A member asking for + // REDACTED gets placeholders too. The server does not evaluate the + // group's listener rules, only that it carries one, so this is the cheap + // way to render a group blurred, and the mode to use whenever the client + // intends to render it blurred regardless of whether the viewer could + // read it in full. + REDACTED = 2; } // Content for a chat message @@ -212,12 +288,23 @@ message Emoji { }]; } -// Reactor identifies a user who reacted to a message and when they did so. +// Reactor identifies a user who reacted to a message with a given emoji, and +// when they did so. message Reactor { common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; - // Timestamp the user added this reaction. + // Timestamp the user added this reaction. Display only: reactors are never + // ordered by it (see version). google.protobuf.Timestamp reacted_ts = 2 [(validate.rules).timestamp.required = true]; + + // 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. + uint64 version = 3 [(validate.rules).uint64.gte = 1]; } // ReactionSummary is the aggregate reaction state attached to a message. It is @@ -242,14 +329,30 @@ message EmojiReaction { // returned here. uint64 count = 2; - // Whether the requesting user reacted with this emoji. Per-viewer: count and - // sample_reactors are shareable across users, but this bit is computed for - // the caller. - bool reacted_by_self = 3; - - // A small sample of reactors, with their reaction timestamps (e.g. for - // rendering a few avatars), capped well below count. The complete, paged - // reactor list is fetched on demand via GetReactors. + // The requesting user's own Reactor entry for this emoji: set exactly when + // they currently react with it, absent when they do not. Per-viewer: count + // and sample_reactors are shareable across users, but this is computed for + // the caller. It is the same entry the reactor list carries, whether or + // not the viewer still sits in sample_reactors, so a client can render + // itself among the reactors, or place itself in a partially loaded reactor + // list by its version, without paging GetReactors to find its own row. + // + // Its presence answers "did I react"; its version is NOT the watermark for + // that toggle. An EmojiReaction is a snapshot at `version`, and every + // transition of the viewer's at or below it is already reflected here, so + // live ReactionUpdates for the viewer are applied against `version` as for + // any other actor. In an AddReactionResponse the entry's version equals + // `version`. In a summary read it is normally at most `version`, but can + // exceed it by one: the viewer's own add landing between the server's + // aggregate read and its per-viewer read. Then this entry is the newer + // truth and the aggregate is one transition behind, which the add's own + // ReactionUpdate or the next refresh reconciles. + 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. repeated Reactor sample_reactors = 4 [(validate.rules).repeated = { max_items: 8 // Sample only; not the full list }]; @@ -259,12 +362,22 @@ message EmojiReaction { // // Opaque to clients, and for ordering only. Apply reaction updates // last-writer-wins by this value per (message, emoji) — and per actor for - // reacted_by_self — and treat a loaded summary as stale when a higher + // self_reactor — and treat a loaded summary as stale when a higher // version arrives. It is compared the same way as chat.v1.RosterSummary. // version. It is NOT the chat event sequence (reactions never advance // that), and it is NOT gapless: a skipped value means nothing, and there is // no delta to fetch against it — a missed update is reconciled by // refreshing the summary on view. + // + // Keep the watermark after the emoji empties. The server retains the + // version across an emoji's count reaching 0 and being re-added, so a + // re-add always arrives with a higher version than the removal; a client + // that forgets the (message, emoji) version when it stops rendering the + // emoji has no way to reject a delayed, lower-versioned ADDED for it, and + // would resurrect an emoji the server has already emptied. Hide the entry, + // keep its version, at least for the session. A summary omits emptied + // emoji entirely — their versions are not surfaced — so a refresh cannot + // restore a watermark once forgotten. uint64 version = 5 [(validate.rules).uint64.gte = 1]; } @@ -280,9 +393,10 @@ message ReactionUpdate { // The emoji that was added or removed. Emoji emoji = 2 [(validate.rules).message.required = true]; - // The user who added or removed the reaction. A client renders - // reacted_by_self by comparing this to itself, so a reaction made on the - // user's other device is reflected. + // 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. common.v1.UserId actor = 3 [(validate.rules).message.required = true]; Action action = 4 [(validate.rules).enum = { @@ -295,19 +409,22 @@ message ReactionUpdate { } // The emoji's total reactor count after this change. 0 means no reactors - // remain and the client should drop the entry from the summary. + // remain: stop rendering the emoji, but keep its version watermark (see + // EmojiReaction.version) so a delayed, lower-versioned ADDED for it is + // still ignored rather than resurrecting it. uint64 count = 5; // The emoji aggregate's version after this change. Clients apply // last-writer-wins by this value: ignore the count if version <= the count - // watermark held, and ignore the actor's reacted_by_self toggle if + // watermark held, and ignore the actor's self_reactor toggle if // version <= the per-actor watermark held. Matches EmojiReaction.version. uint64 version = 6 [(validate.rules).uint64.gte = 1]; // When the actor reacted. On ADDED, clients record this as the actor's - // Reactor.reacted_ts (e.g. when slotting them into sample_reactors); ignored - // for REMOVED. This is a display timestamp, distinct from `sequence`, which - // is the ordering key. + // 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. google.protobuf.Timestamp reacted_ts = 7 [(validate.rules).timestamp.required = true]; } diff --git a/proto/push/v1/model.proto b/proto/push/v1/model.proto index f3e7a5c..29df45e 100644 --- a/proto/push/v1/model.proto +++ b/proto/push/v1/model.proto @@ -82,4 +82,9 @@ message ChatMetadata { // The chat message that was sent, if the push is for a message messaging.v1.Message message = 3; + + // Whether the recipient had this chat muted when the push was sent. + // The push is still delivered so the client can store the message, + // but the client must not present a notification for it. + bool muted = 4; } diff --git a/proto/reporting/v1/reporting_service.proto b/proto/reporting/v1/reporting_service.proto new file mode 100644 index 0000000..6ae0ad9 --- /dev/null +++ b/proto/reporting/v1/reporting_service.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package flipcash.reporting.v1; + +import "blob/v1/model.proto"; +import "common/v1/common.proto"; +import "messaging/v1/model.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/reporting/v1;reportingpb"; +option java_package = "com.codeinc.flipcash.gen.reporting.v1"; +option objc_class_prefix = "FPBReportingV1"; + +// Reporting lets users flag content or other users for review. +service Reporting { + // Report files a report against a user, chat, message, or blob. Reports + // are advisory: filing one has no immediate client-visible effect and + // outcomes are not surfaced back to the reporter. Reporting the same + // target more than once is a no-op and returns OK. + rpc Report(ReportRequest) returns (ReportResponse); +} + +message ReportRequest { + // The target being reported + oneof target { + option (validate.required) = true; + + // A user + common.v1.UserId user_id = 1; + + // A chat + common.v1.ChatId chat_id = 2; + + // A message within a chat + ReportedMessage message = 3; + + // A blob + blob.v1.BlobId blob_id = 4; + } + + // Optional free-form text from the reporter describing the reason for + // the report. Not moderated or shown to other users. + string description = 5 [(validate.rules).string = { + max_len: 8192 + }]; + + // ReportedMessage identifies a message. MessageId is a per-chat sequence + // number, so the chat it belongs to must be provided alongside it. + message ReportedMessage { + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + messaging.v1.MessageId message_id = 2 [(validate.rules).message.required = true]; + } + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message ReportResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + NOT_FOUND = 2; + } +} From 56759f02a3c181645cdaf07f9a22a1a3a5c60586 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 18 Sep 2026 16:05:56 -0400 Subject: [PATCH 2/3] docs(changelog): add the 0.10.0 entry --- CHANGELOG.md | 108 ++++++++++++++++++ .../ContractInfo.swift | 2 +- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 310e60f..73f3aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,114 @@ 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.10.0 + +Synced to [`flipcash2-protobuf-api@dd5e92db`](https://github.com/code-payments/flipcash2-protobuf-api/commit/dd5e92db6f76700ab6b565f4eff1f9fb25140c9a), +picking up [#103](https://github.com/code-payments/flipcash2-protobuf-api/pull/103) through +[#113](https://github.com/code-payments/flipcash2-protobuf-api/pull/113) — eleven upstream commits, +the largest sync so far. + +One field changes type in place, so this upgrade is not free. Everything else is additive: chat +muting, a Reporting service, and a redaction model that lets a non-member preview a group without +being shown what it says. + +### Breaking + +`messaging.v1.EmojiReaction.reacted_by_self` (`bool`, field 3) is replaced by `self_reactor` +(`Reactor`, field 3). Same field number, different wire type — varint to length-delimited — so this +is not a field that can be read either way. A 0.9.0 client parsing a 0.10.0 response does not see a +missing field, it sees a malformed one. + +The replacement carries more than the bit it replaces. `self_reactor` is set exactly when the viewer +currently reacts with that emoji, and it is the same `Reactor` entry the reactor list carries, so a +client can place itself in a partially loaded reactor list by its `version` instead of paging +`GetReactors` to find its own row. Its presence answers "did I react". Its version is not the +watermark for that toggle: an `EmojiReaction` is a snapshot at `version`, every transition of the +viewer's at or below it is already reflected, and live `ReactionUpdate`s for the viewer are applied +against `EmojiReaction.version` like any other actor's. + +### Added + +Chat muting, on `chat.v1.Chat`: + +- `MuteChat(MuteChatRequest) returns (MuteChatResponse)` and + `UnmuteChat(UnmuteChatRequest) returns (UnmuteChatResponse)`. Both results are `OK`, `DENIED` + (not a member) and `NOT_FOUND`. Both responses carry `viewer_state` when `OK`, so the caller can + apply the new state at its version without a refetch. +- `MuteState`, a required `oneof duration` of `google.protobuf.Timestamp until` or an empty + `Forever`. A timed mute must end in the future or the server rejects it. +- `ViewerState`, holding `Settings.mute` and a `version` that advances by one on every real change. + It is compared like `RosterSummary.version` — apply the greater, drop the rest — and it is + private to the viewer, so it never moves the roster's version. +- `Metadata.viewer_state` (field 12), absent when the chat holds nothing about the viewer. +- `MetadataUpdate.ViewerStateChanged` (oneof case 3), which is how a mute made on another device + arrives. +- `push.v1.ChatMetadata.muted` (field 4). + +A Reporting service, in the new `flipcash.reporting.v1` package: + +- `Report(ReportRequest) returns (ReportResponse)` over a required target `oneof` of `user_id`, + `chat_id`, `message` or `blob_id`, plus an optional free-form `description` capped at 8192 bytes. + A reported message travels with its chat, because `MessageId` is a per-chat sequence number. +- Reports are advisory. Nothing about the client's view changes, no outcome is reported back, and + reporting the same target twice is a no-op that returns `OK`. + +Redacted reads, so a viewer can be shown that a chat exists without being shown what it says: + +- `messaging.v1.ViewMode`, with `FULL = 0`, `FULL_OR_REDACTED = 1` and `REDACTED = 2`. It is set on + every read that returns a `Message` — `GetMessage`, `GetMessages`, `GetDelta`, `GetChat` for + `last_message`, and a preview stream. `FULL = 0` is the contract that predates redaction, so a + client that does not set the field behaves exactly as before. +- `messaging.v1.Message.redacted` (field 9), set on a copy that was redacted for the viewer. The + content keeps its kind and structure but holds placeholders: text of the same script, length and + line structure; media with its dimensions and blurhash; a reply with a placeholder body. Cash, + system and deleted content are never redacted. +- `event.v1.StreamEventsRequest.Params.chat_preview`, a stream targeted at one group chat under a + `ViewMode` for a server-fixed window, ending in `STREAM_EXPIRED`. Group chats only; a DM is + `DENIED` whatever the viewer's standing. +- `StreamEventsResponse.Error.Code` gains `NOT_FOUND = 2` and `STREAM_EXPIRED = 3`. +- `Reactor.version` (field 3) and `GetReactorsResponse.version` (field 5). + +### Changed + +`blob.v1.BlobMetadata.download_url` (field 3) is no longer `required`. A redacted message's +renditions carry their intrinsic metadata — for an image, dimensions and blurhash — with no URL. +The blob is not readable by that viewer, so `GetBlobs` would not return one either. + +Reactor lists are now ordered by `Reactor.version` descending, newest first. `reacted_ts` is display +only and no longer an ordering key; `GetReactorsRequest.options.order` is ignored and `page_size` +above 100 is clamped to 100. + +### Upgrading + +Three things need attention beyond the rename. + +**Keep an emoji's version watermark after its count reaches 0.** The server retains the version +across an emoji emptying and being re-added, so a re-add always arrives above the removal. A client +that drops the `(message, emoji)` version when it stops rendering the emoji has nothing to reject a +delayed, lower-versioned `ADDED` with, and resurrects an emoji the server has already emptied. Hide +the entry, keep its version. A summary omits emptied emoji entirely, so once the watermark is +forgotten a refresh cannot restore it. + +**Muted pushes are still delivered.** The server sends them so the client can store the message, and +flags them with `ChatMetadata.muted`; suppressing the notification is the client's job. Nothing is +sent when a timed mute lapses either, so the client owns that countdown. A mute is cleared +server-side when the caller leaves the chat. + +**Redacted and unredacted copies of a chat are not one history.** A redacted copy is not a version of +the message — `event_sequence` still describes the underlying message — so a client that later reads +it unredacted replaces the placeholder because the read was unredacted, not because of a higher +sequence. Key the cache by the `ViewMode` the read was made under and keep the two apart. A client +catching up a redacted view must call `GetDelta` under the same mode it read the history under, and +must not offer to copy, quote or download a redacted message. + +### Unchanged + +`reacted_by_self` is the only field that changed number or type, and nothing was renumbered. The two +new `StreamEventsResponse.Error.Code` cases are appended after `DENIED` and `INVALID_TIMESTAMP`, so +a positional `rawValue` mapping over that enum does not shift. No service, RPC, message or enum case +was removed. + ## 0.9.0 No contract change. Still synced to diff --git a/Sources/Flipcash2ClientProtocol/ContractInfo.swift b/Sources/Flipcash2ClientProtocol/ContractInfo.swift index dce6762..e95c012 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.9.0-dev" + public static let version = "0.10.0-dev" public static let protoCommit = "dd5e92db6f76700ab6b565f4eff1f9fb25140c9a" public static var isLocal: Bool { protoCommit == localSentinel } From 4b600ef3686f8004de6dcc66933eccdf5721f7a3 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 18 Sep 2026 16:48:09 -0400 Subject: [PATCH 3/3] build(swift): pin local builds to Xcode's toolchain swiftly shims `swift` to whichever swift.org toolchain is the user's default, and none of them can build this package: grpc-swift-protobuf's codegen plugin and swift-protobuf both fail to resolve Foundation's `Data` against the macOS SDK. Reproduced on 6.2.1 and 6.3.3; Apple's Swift 6.4 from Xcode builds it clean. CI already runs bare `swift build` against Xcode's toolchain, so this only makes a local checkout match what CI does. `.swift-version` is read by swiftly and ignored by everything else, so CI and contributors without swiftly are unaffected. --- .swift-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .swift-version diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..7e58507 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +xcode