diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f3aa9..da10b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,88 @@ 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.11.0 + +Synced to [`flipcash2-protobuf-api@4ccbbe43`](https://github.com/code-payments/flipcash2-protobuf-api/commit/4ccbbe43197ec6cc2d7a3f0681a73f799cdcfb46), +picking up [#114](https://github.com/code-payments/flipcash2-protobuf-api/pull/114) through +[#116](https://github.com/code-payments/flipcash2-protobuf-api/pull/116). Only `chat.v1` moved. + +Additive throughout: a chat's roster becomes readable page by page, a group chat becomes editable, +and the server starts telling each viewer what it will let them do. Nothing existing changed number, +type or meaning, so the upgrade itself is free. Two of the additions carry client obligations that +are not visible in the generated code; those are under Upgrading. + +### Added + +Roster paging, on `chat.v1.Chat`: + +- `GetRoster(GetRosterRequest) returns (GetRosterResponse)` pages a chat's roster, most recently + joined first. Page size comes from `common.v1.QueryOptions` and is capped at 100; ordering is + fixed and not client-selectable. The `common.v1.PagingToken` is opaque, server-generated and bound + to `chat_id` — it is refused with another chat. `GetRosterResponse.Result` is `OK`, `DENIED`, + `NOT_FOUND`. A member may read the roster, as may a non-member the group's listener rules admit; a + viewer who may only preview the chat is `DENIED`. +- Every page carries the chat's `RosterSummary`, so `member_count` and `version` arrive with the + members rather than needing a separate read. +- Pointers are hydrated for a DM's participants only. A group's members carry none, because group + pointer advances are never broadcast and a page of them would be stale as soon as it was served. +- `Member.joined_at` (field 4) and `Member.version` (field 5). `joined_at` is when the member most + recently joined, so a member who left and rejoined carries the rejoin time. `version` is the + roster version that placed them — equal to `roster_summary.version` on the + `RosterUpdate.MemberJoined` that announced it, and zero for a member present at the chat's + creation and for every DM participant. Both are set on a `GetRoster` page and on `MemberJoined`, + and unset on `Metadata.members`, which carries only the viewer's own entry. + +Group chat editing, on `chat.v1.Chat`: + +- `EditChat(EditChatRequest) returns (EditChatResponse)` changes a group chat's `title`, `picture`, + or both. Every field is optional and only the ones set are changed. The edit is atomic, so a + refusal of any part applies none of it, and a request that sets nothing returns `OK`. +- `EditChatResponse.Result` is `OK`, `DENIED`, `NOT_FOUND`, `TITLE_MODERATED`, + `PICTURE_BLOB_NOT_ACCEPTED`. `TITLE_MODERATED` carries the tripped `moderation.v1.FlaggedCategory` + in `flagged_category`; every other result leaves it `NONE`. `OK` carries the post-edit `Metadata` + in `chat`, including the renditions the server derived. +- A new picture is a blob the caller has already uploaded through BlobStorage: the client uploads + only the ORIGINAL and passes its `BlobId` once the blob is `READY`. +- `MetadataUpdate.TitleChanged` (oneof case 4) and `MetadataUpdate.PictureChanged` (oneof case 5), + one per field actually changed, delivered to the chat's members including the editor's other + devices. + +Per-viewer permissions: + +- `ViewerState.permissions` (field 2) and the nested `ViewerState.Permissions`, whose only flag so + far is `can_edit` — whether the viewer may call `EditChat` on this chat. Each flag is named for + the RPC it gates and defaults to false, so an unset flag always means the action is not permitted. +- `ViewerState` is now documented as always present for a member. It remains absent when the chat + holds nothing about the viewer. + +### Upgrading + +Nothing forces a change. Both obligations below arrive only with the features that carry them. + +**A `GetRoster` page is not authoritative on its own.** A DM's roster and a small group's are read +whole, at exactly `roster_summary.version`. A large group's is paged from an index that trails +membership writes, so a page may lag its own summary: a member who just joined may be absent, one +who just left may be present. The client cannot see which case it is in, so it must follow the +weaker contract — merge each page against what the event stream has already delivered, per member, +by `Member.version`, the greater winning. A member absent from a fully read roster is gone unless +the client holds their join above that page's `roster_summary.version`, and a join that lands during +the walk arrives only as a `RosterUpdate`. + +**`can_edit` is not derivable.** It is computed from the viewer's standing in the chat and the +chat's rules, neither of which the client holds in full. Show the edit affordance if and only if the +flag is set, rather than inferring it from membership or chat kind, and expect it to move: a change +advances `ViewerState.version` and arrives as `MetadataUpdate.ViewerStateChanged`. + +### Unchanged + +Nothing was renumbered, and nothing existing changed type or meaning. `Member.joined_at` and +`Member.version` are appended after `pointers` (field 3); the two `MetadataUpdate` cases are +appended after `viewer_state_changed` (case 3); `ViewerState.permissions` takes the free field 2 +between `settings` (1) and `version` (10), which does not move. Both new `Result` enums belong to +new messages, so no positional `rawValue` mapping over an existing enum shifts. No service, RPC, +message or enum case was removed. Everything else in the diff is comments. + ## 0.10.0 Synced to [`flipcash2-protobuf-api@dd5e92db`](https://github.com/code-payments/flipcash2-protobuf-api/commit/dd5e92db6f76700ab6b565f4eff1f9fb25140c9a), diff --git a/Sources/Flipcash2ClientProtocol/ContractInfo.swift b/Sources/Flipcash2ClientProtocol/ContractInfo.swift index e95c012..d2385d6 100644 --- a/Sources/Flipcash2ClientProtocol/ContractInfo.swift +++ b/Sources/Flipcash2ClientProtocol/ContractInfo.swift @@ -1,8 +1,8 @@ // Generated by scripts/generate-swift.sh. Do not edit. public enum Flipcash2ContractInfo { - public static let version = "0.10.0-dev" - public static let protoCommit = "dd5e92db6f76700ab6b565f4eff1f9fb25140c9a" + public static let version = "0.11.0-dev" + public static let protoCommit = "4ccbbe43197ec6cc2d7a3f0681a73f799cdcfb46" public static var isLocal: Bool { protoCommit == localSentinel } diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift index 50f6a18..a5ae346 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift @@ -56,6 +56,18 @@ public enum Flipcash_Chat_V1_Chat { method: "GetGroupChatFeed" ) } + /// Namespace for "GetRoster" metadata. + public enum GetRoster { + /// Request type for "GetRoster". + public typealias Input = Flipcash_Chat_V1_GetRosterRequest + /// Response type for "GetRoster". + public typealias Output = Flipcash_Chat_V1_GetRosterResponse + /// Descriptor for "GetRoster". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.chat.v1.Chat"), + method: "GetRoster" + ) + } /// Namespace for "StartChat" metadata. public enum StartChat { /// Request type for "StartChat". @@ -92,6 +104,18 @@ public enum Flipcash_Chat_V1_Chat { method: "LeaveChat" ) } + /// Namespace for "EditChat" metadata. + public enum EditChat { + /// Request type for "EditChat". + public typealias Input = Flipcash_Chat_V1_EditChatRequest + /// Response type for "EditChat". + public typealias Output = Flipcash_Chat_V1_EditChatResponse + /// Descriptor for "EditChat". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.chat.v1.Chat"), + method: "EditChat" + ) + } /// Namespace for "MuteChat" metadata. public enum MuteChat { /// Request type for "MuteChat". @@ -121,9 +145,11 @@ public enum Flipcash_Chat_V1_Chat { GetChat.descriptor, GetDmChatFeed.descriptor, GetGroupChatFeed.descriptor, + GetRoster.descriptor, StartChat.descriptor, JoinChat.descriptor, LeaveChat.descriptor, + EditChat.descriptor, MuteChat.descriptor, UnmuteChat.descriptor ] @@ -256,6 +282,55 @@ extension Flipcash_Chat_V1_Chat { onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable + /// Call the "GetRoster" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetRoster pages a chat's roster, most recently joined first. Every + /// > page carries the chat's RosterSummary, and every member carries the + /// > roster version that placed them (see Member.version). + /// > + /// > A DM's roster is its participants, and a small group's is read whole: + /// > for these a page is the roster at exactly roster_summary.version — a + /// > roster that fits one page is member_count members, and a later page + /// > is a fresh snapshot at its own summary. A large group's roster is + /// > paged from an index that trails membership writes briefly, so a page + /// > may lag roster_summary — a member + /// > who just joined may be absent, one who just left may be present. + /// > Clients do not see which case they are in and must follow the weaker + /// > contract: treat roster_summary.version as the staleness watermark + /// > described on RosterSummary, and merge each page against what the + /// > event stream has told them per member by Member.version, the greater + /// > winning. A cached member absent from a fully read roster is gone + /// > unless the client holds their join at a version above the page's + /// > roster_summary.version. A join during the walk lands ahead of the + /// > cursor and arrives only as a RosterUpdate. + /// > + /// > Pointers are hydrated for a DM's participants only. A group's members + /// > carry none: group pointer advances are never broadcast, so a page of + /// > them would be stale as soon as it was served. + /// > + /// > Requires that the caller may read the chat: a member, or a non-member + /// > a group's listener rules admit. A viewer who may only preview the chat + /// > is DENIED. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_GetRosterRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_GetRosterRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_GetRosterResponse` 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 getRoster( + 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 "StartChat" method. /// /// > Source IDL Documentation: @@ -325,6 +400,45 @@ extension Flipcash_Chat_V1_Chat { onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable + /// Call the "EditChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > EditChat edits a group chat's record. Every editable field is optional; + /// > only the ones set in the request are changed, and the edit is atomic: + /// > if any part is refused, nothing is applied. + /// > + /// > Only a group chat may be edited, and only by a member the server permits + /// > to edit it, as reported by ViewerState.Permissions.can_edit; anyone else + /// > is DENIED. A new title is moderated like + /// > StartChat's. A new picture is a blob the caller has already uploaded via + /// > BlobStorage: the client uploads only the ORIGINAL and passes the + /// > resulting BlobId once the blob is READY, and the server derives the + /// > remaining renditions. Setting a field to the value the chat already has + /// > is a no-op for that field, and a request that sets nothing is a no-op + /// > that returns OK. + /// > + /// > Every real change reaches the chat's members, including the caller's + /// > other devices, on the event stream as one MetadataUpdate per field + /// > changed: TitleChanged for the title, PictureChanged for the picture. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_EditChatRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_EditChatRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_EditChatResponse` 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 editChat( + 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 "MuteChat" method. /// /// > Source IDL Documentation: @@ -542,6 +656,66 @@ extension Flipcash_Chat_V1_Chat { ) } + /// Call the "GetRoster" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetRoster pages a chat's roster, most recently joined first. Every + /// > page carries the chat's RosterSummary, and every member carries the + /// > roster version that placed them (see Member.version). + /// > + /// > A DM's roster is its participants, and a small group's is read whole: + /// > for these a page is the roster at exactly roster_summary.version — a + /// > roster that fits one page is member_count members, and a later page + /// > is a fresh snapshot at its own summary. A large group's roster is + /// > paged from an index that trails membership writes briefly, so a page + /// > may lag roster_summary — a member + /// > who just joined may be absent, one who just left may be present. + /// > Clients do not see which case they are in and must follow the weaker + /// > contract: treat roster_summary.version as the staleness watermark + /// > described on RosterSummary, and merge each page against what the + /// > event stream has told them per member by Member.version, the greater + /// > winning. A cached member absent from a fully read roster is gone + /// > unless the client holds their join at a version above the page's + /// > roster_summary.version. A join during the walk lands ahead of the + /// > cursor and arrives only as a RosterUpdate. + /// > + /// > Pointers are hydrated for a DM's participants only. A group's members + /// > carry none: group pointer advances are never broadcast, so a page of + /// > them would be stale as soon as it was served. + /// > + /// > Requires that the caller may read the chat: a member, or a non-member + /// > a group's listener rules admit. A viewer who may only preview the chat + /// > is DENIED. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_GetRosterRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_GetRosterRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_GetRosterResponse` 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 getRoster( + 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.GetRoster.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + /// Call the "StartChat" method. /// /// > Source IDL Documentation: @@ -644,6 +818,56 @@ extension Flipcash_Chat_V1_Chat { ) } + /// Call the "EditChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > EditChat edits a group chat's record. Every editable field is optional; + /// > only the ones set in the request are changed, and the edit is atomic: + /// > if any part is refused, nothing is applied. + /// > + /// > Only a group chat may be edited, and only by a member the server permits + /// > to edit it, as reported by ViewerState.Permissions.can_edit; anyone else + /// > is DENIED. A new title is moderated like + /// > StartChat's. A new picture is a blob the caller has already uploaded via + /// > BlobStorage: the client uploads only the ORIGINAL and passes the + /// > resulting BlobId once the blob is READY, and the server derives the + /// > remaining renditions. Setting a field to the value the chat already has + /// > is a no-op for that field, and a request that sets nothing is a no-op + /// > that returns OK. + /// > + /// > Every real change reaches the chat's members, including the caller's + /// > other devices, on the event stream as one MetadataUpdate per field + /// > changed: TitleChanged for the title, PictureChanged for the picture. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_EditChatRequest` message. + /// - serializer: A serializer for `Flipcash_Chat_V1_EditChatRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Chat_V1_EditChatResponse` 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 editChat( + 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.EditChat.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + /// Call the "MuteChat" method. /// /// > Source IDL Documentation: @@ -856,6 +1080,61 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { ) } + /// Call the "GetRoster" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetRoster pages a chat's roster, most recently joined first. Every + /// > page carries the chat's RosterSummary, and every member carries the + /// > roster version that placed them (see Member.version). + /// > + /// > A DM's roster is its participants, and a small group's is read whole: + /// > for these a page is the roster at exactly roster_summary.version — a + /// > roster that fits one page is member_count members, and a later page + /// > is a fresh snapshot at its own summary. A large group's roster is + /// > paged from an index that trails membership writes briefly, so a page + /// > may lag roster_summary — a member + /// > who just joined may be absent, one who just left may be present. + /// > Clients do not see which case they are in and must follow the weaker + /// > contract: treat roster_summary.version as the staleness watermark + /// > described on RosterSummary, and merge each page against what the + /// > event stream has told them per member by Member.version, the greater + /// > winning. A cached member absent from a fully read roster is gone + /// > unless the client holds their join at a version above the page's + /// > roster_summary.version. A join during the walk lands ahead of the + /// > cursor and arrives only as a RosterUpdate. + /// > + /// > Pointers are hydrated for a DM's participants only. A group's members + /// > carry none: group pointer advances are never broadcast, so a page of + /// > them would be stale as soon as it was served. + /// > + /// > Requires that the caller may read the chat: a member, or a non-member + /// > a group's listener rules admit. A viewer who may only preview the chat + /// > is DENIED. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_GetRosterRequest` 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 getRoster( + 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.getRoster( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + /// Call the "StartChat" method. /// /// > Source IDL Documentation: @@ -943,6 +1222,51 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { ) } + /// Call the "EditChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > EditChat edits a group chat's record. Every editable field is optional; + /// > only the ones set in the request are changed, and the edit is atomic: + /// > if any part is refused, nothing is applied. + /// > + /// > Only a group chat may be edited, and only by a member the server permits + /// > to edit it, as reported by ViewerState.Permissions.can_edit; anyone else + /// > is DENIED. A new title is moderated like + /// > StartChat's. A new picture is a blob the caller has already uploaded via + /// > BlobStorage: the client uploads only the ORIGINAL and passes the + /// > resulting BlobId once the blob is READY, and the server derives the + /// > remaining renditions. Setting a field to the value the chat already has + /// > is a no-op for that field, and a request that sets nothing is a no-op + /// > that returns OK. + /// > + /// > Every real change reaches the chat's members, including the caller's + /// > other devices, on the event stream as one MetadataUpdate per field + /// > changed: TitleChanged for the title, PictureChanged for the picture. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Chat_V1_EditChatRequest` 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 editChat( + 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.editChat( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + /// Call the "MuteChat" method. /// /// > Source IDL Documentation: @@ -1156,6 +1480,65 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { ) } + /// Call the "GetRoster" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetRoster pages a chat's roster, most recently joined first. Every + /// > page carries the chat's RosterSummary, and every member carries the + /// > roster version that placed them (see Member.version). + /// > + /// > A DM's roster is its participants, and a small group's is read whole: + /// > for these a page is the roster at exactly roster_summary.version — a + /// > roster that fits one page is member_count members, and a later page + /// > is a fresh snapshot at its own summary. A large group's roster is + /// > paged from an index that trails membership writes briefly, so a page + /// > may lag roster_summary — a member + /// > who just joined may be absent, one who just left may be present. + /// > Clients do not see which case they are in and must follow the weaker + /// > contract: treat roster_summary.version as the staleness watermark + /// > described on RosterSummary, and merge each page against what the + /// > event stream has told them per member by Member.version, the greater + /// > winning. A cached member absent from a fully read roster is gone + /// > unless the client holds their join at a version above the page's + /// > roster_summary.version. A join during the walk lands ahead of the + /// > cursor and arrives only as a RosterUpdate. + /// > + /// > Pointers are hydrated for a DM's participants only. A group's members + /// > carry none: group pointer advances are never broadcast, so a page of + /// > them would be stale as soon as it was served. + /// > + /// > Requires that the caller may read the chat: a member, or a non-member + /// > a group's listener rules admit. A viewer who may only preview the chat + /// > is DENIED. + /// + /// - 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 getRoster( + _ message: Flipcash_Chat_V1_GetRosterRequest, + 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.getRoster( + request: request, + options: options, + onResponse: handleResponse + ) + } + /// Call the "StartChat" method. /// /// > Source IDL Documentation: @@ -1255,6 +1638,55 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { ) } + /// Call the "EditChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > EditChat edits a group chat's record. Every editable field is optional; + /// > only the ones set in the request are changed, and the edit is atomic: + /// > if any part is refused, nothing is applied. + /// > + /// > Only a group chat may be edited, and only by a member the server permits + /// > to edit it, as reported by ViewerState.Permissions.can_edit; anyone else + /// > is DENIED. A new title is moderated like + /// > StartChat's. A new picture is a blob the caller has already uploaded via + /// > BlobStorage: the client uploads only the ORIGINAL and passes the + /// > resulting BlobId once the blob is READY, and the server derives the + /// > remaining renditions. Setting a field to the value the chat already has + /// > is a no-op for that field, and a request that sets nothing is a no-op + /// > that returns OK. + /// > + /// > Every real change reaches the chat's members, including the caller's + /// > other devices, on the event stream as one MetadataUpdate per field + /// > changed: TitleChanged for the title, PictureChanged for the picture. + /// + /// - 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 editChat( + _ message: Flipcash_Chat_V1_EditChatRequest, + 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.editChat( + request: request, + options: options, + onResponse: handleResponse + ) + } + /// Call the "MuteChat" method. /// /// > Source IDL Documentation: diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift index f5ba6ab..f265280 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift @@ -360,6 +360,144 @@ public struct Flipcash_Chat_V1_GetGroupChatFeedResponse: Sendable { fileprivate var _pagingToken: Flipcash_Common_V1_PagingToken? = nil } +public struct Flipcash_Chat_V1_GetRosterRequest: 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} + + /// QueryOptions controls page_size, capped at 100. Ordering is fixed to + /// most recently joined first and is not client-selectable. + /// + /// Leave query_options.paging_token unset on the first request. On every + /// subsequent request, set it to the paging_token from the most recent + /// response. The token is opaque and server-generated; do not construct + /// it. It is bound to chat_id and refused with another chat. + public var queryOptions: Flipcash_Common_V1_QueryOptions { + get {return _queryOptions ?? Flipcash_Common_V1_QueryOptions()} + set {_queryOptions = newValue} + } + /// Returns true if `queryOptions` has been explicitly set. + public var hasQueryOptions: Bool {return self._queryOptions != nil} + /// Clears the value of `queryOptions`. Subsequent reads from it will return its default value. + public mutating func clearQueryOptions() {self._queryOptions = 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 _queryOptions: Flipcash_Common_V1_QueryOptions? = nil + fileprivate var _auth: Flipcash_Common_V1_Auth? = nil +} + +public struct Flipcash_Chat_V1_GetRosterResponse: 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_GetRosterResponse.Result = .ok + + /// The page, most recently joined first, each with user_profile, + /// joined_at and version set, and pointers for a DM. Set when result is + /// OK. + public var members: [Flipcash_Chat_V1_Member] = [] + + /// The chat's roster summary as of this page's read. Set when result is + /// OK. Applied by version as described on RosterSummary; see GetRoster + /// for how the page relates to it. + public var rosterSummary: Flipcash_Chat_V1_RosterSummary { + get {return _rosterSummary ?? Flipcash_Chat_V1_RosterSummary()} + set {_rosterSummary = newValue} + } + /// Returns true if `rosterSummary` has been explicitly set. + public var hasRosterSummary: Bool {return self._rosterSummary != nil} + /// Clears the value of `rosterSummary`. Subsequent reads from it will return its default value. + public mutating func clearRosterSummary() {self._rosterSummary = nil} + + /// PagingToken carries the cursor over (joined_at, user_id) after the last + /// member returned. The client MUST send it back in + /// query_options.paging_token on the next GetRosterRequest. Set when + /// result is OK and has_more is true. + public var pagingToken: Flipcash_Common_V1_PagingToken { + get {return _pagingToken ?? Flipcash_Common_V1_PagingToken()} + set {_pagingToken = newValue} + } + /// Returns true if `pagingToken` has been explicitly set. + public var hasPagingToken: Bool {return self._pagingToken != nil} + /// Clears the value of `pagingToken`. Subsequent reads from it will return its default value. + public mutating func clearPagingToken() {self._pagingToken = nil} + + /// HasMore indicates whether further pages remain. When false, the roster + /// has been fully read as of this page; the complete member list is this + /// set reconciled with the event stream (see GetRoster). When true, the + /// client should issue another GetRosterRequest with the returned + /// paging_token. + public var hasMore_p: Bool = false + + 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_GetRosterResponse.Result] = [ + .ok, + .denied, + .notFound, + ] + + } + + public init() {} + + fileprivate var _rosterSummary: Flipcash_Chat_V1_RosterSummary? = nil + fileprivate var _pagingToken: Flipcash_Common_V1_PagingToken? = nil +} + public struct Flipcash_Chat_V1_StartChatRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -713,6 +851,172 @@ public struct Flipcash_Chat_V1_LeaveChatResponse: Sendable { public init() {} } +public struct Flipcash_Chat_V1_EditChatRequest: 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} + + /// The new title. Left unchanged when unset. + public var title: Flipcash_Chat_V1_EditChatRequest.Title { + get {return _title ?? Flipcash_Chat_V1_EditChatRequest.Title()} + set {_title = newValue} + } + /// Returns true if `title` has been explicitly set. + public var hasTitle: Bool {return self._title != nil} + /// Clears the value of `title`. Subsequent reads from it will return its default value. + public mutating func clearTitle() {self._title = nil} + + /// The new picture. Left unchanged when unset. + public var picture: Flipcash_Chat_V1_EditChatRequest.Picture { + get {return _picture ?? Flipcash_Chat_V1_EditChatRequest.Picture()} + set {_picture = newValue} + } + /// Returns true if `picture` has been explicitly set. + public var hasPicture: Bool {return self._picture != nil} + /// Clears the value of `picture`. Subsequent reads from it will return its default value. + public mutating func clearPicture() {self._picture = 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 struct Title: 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 value: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + + public struct Picture: 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 blob holding the ORIGINAL picture the caller uploaded. It must be + /// owned by the caller and READY; the server derives the remaining + /// renditions from it. + public var blobID: Flipcash_Blob_V1_BlobId { + get {return _blobID ?? Flipcash_Blob_V1_BlobId()} + set {_blobID = newValue} + } + /// Returns true if `blobID` has been explicitly set. + public var hasBlobID: Bool {return self._blobID != nil} + /// Clears the value of `blobID`. Subsequent reads from it will return its default value. + public mutating func clearBlobID() {self._blobID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _blobID: Flipcash_Blob_V1_BlobId? = nil + } + + public init() {} + + fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil + fileprivate var _title: Flipcash_Chat_V1_EditChatRequest.Title? = nil + fileprivate var _picture: Flipcash_Chat_V1_EditChatRequest.Picture? = nil + fileprivate var _auth: Flipcash_Common_V1_Auth? = nil +} + +public struct Flipcash_Chat_V1_EditChatResponse: 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_EditChatResponse.Result = .ok + + /// The metadata for the chat after the edit, as seen by the caller, + /// including any picture renditions the server derived. Set only when + /// result == OK. + public var chat: Flipcash_Chat_V1_Metadata { + get {return _chat ?? Flipcash_Chat_V1_Metadata()} + set {_chat = newValue} + } + /// Returns true if `chat` has been explicitly set. + public var hasChat: Bool {return self._chat != nil} + /// Clears the value of `chat`. Subsequent reads from it will return its default value. + public mutating func clearChat() {self._chat = nil} + + /// The best-fit category that tripped moderation, mirroring the Moderation + /// service's vocabulary. Set only when result == TITLE_MODERATED; NONE + /// otherwise. + public var flaggedCategory: Flipcash_Moderation_V1_FlaggedCategory = .none + + 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 titleModerated // = 3 + case pictureBlobNotAccepted // = 4 + 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 + case 3: self = .titleModerated + case 4: self = .pictureBlobNotAccepted + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .ok: return 0 + case .denied: return 1 + case .notFound: return 2 + case .titleModerated: return 3 + case .pictureBlobNotAccepted: return 4 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Flipcash_Chat_V1_EditChatResponse.Result] = [ + .ok, + .denied, + .notFound, + .titleModerated, + .pictureBlobNotAccepted, + ] + + } + + public init() {} + + fileprivate var _chat: Flipcash_Chat_V1_Metadata? = nil +} + 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 @@ -1194,6 +1498,108 @@ extension Flipcash_Chat_V1_GetGroupChatFeedResponse.Result: SwiftProtobuf._Proto 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_GetRosterRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".GetRosterRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}query_options\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._queryOptions) }() + 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._queryOptions { + 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_GetRosterRequest, rhs: Flipcash_Chat_V1_GetRosterRequest) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs._queryOptions != rhs._queryOptions {return false} + if lhs._auth != rhs._auth {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_GetRosterResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".GetRosterResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{1}members\0\u{3}roster_summary\0\u{3}paging_token\0\u{3}has_more\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.decodeRepeatedMessageField(value: &self.members) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._rosterSummary) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._pagingToken) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.hasMore_p) }() + 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) + } + if !self.members.isEmpty { + try visitor.visitRepeatedMessageField(value: self.members, fieldNumber: 2) + } + try { if let v = self._rosterSummary { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._pagingToken { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + if self.hasMore_p != false { + try visitor.visitSingularBoolField(value: self.hasMore_p, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_GetRosterResponse, rhs: Flipcash_Chat_V1_GetRosterResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs.members != rhs.members {return false} + if lhs._rosterSummary != rhs._rosterSummary {return false} + if lhs._pagingToken != rhs._pagingToken {return false} + if lhs.hasMore_p != rhs.hasMore_p {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_GetRosterResponse.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_StartChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".StartChatRequest" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}group\0\u{4}\u{8}idempotency_key\0\u{1}auth\0") @@ -1497,6 +1903,167 @@ extension Flipcash_Chat_V1_LeaveChatResponse.Result: SwiftProtobuf._ProtoNamePro 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_EditChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".EditChatRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}title\0\u{1}picture\0\u{2}\u{7}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._title) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._picture) }() + 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._title { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._picture { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + 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_EditChatRequest, rhs: Flipcash_Chat_V1_EditChatRequest) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs._title != rhs._title {return false} + if lhs._picture != rhs._picture {return false} + if lhs._auth != rhs._auth {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_EditChatRequest.Title: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_EditChatRequest.protoMessageName + ".Title" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}value\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.decodeSingularStringField(value: &self.value) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.value.isEmpty { + try visitor.visitSingularStringField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_EditChatRequest.Title, rhs: Flipcash_Chat_V1_EditChatRequest.Title) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_EditChatRequest.Picture: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_EditChatRequest.protoMessageName + ".Picture" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}blob_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._blobID) }() + 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._blobID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_EditChatRequest.Picture, rhs: Flipcash_Chat_V1_EditChatRequest.Picture) -> Bool { + if lhs._blobID != rhs._blobID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_EditChatResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".EditChatResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{1}chat\0\u{3}flagged_category\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._chat) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.flaggedCategory) }() + 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._chat { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if self.flaggedCategory != .none { + try visitor.visitSingularEnumField(value: self.flaggedCategory, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_EditChatResponse, rhs: Flipcash_Chat_V1_EditChatResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs._chat != rhs._chat {return false} + if lhs.flaggedCategory != rhs.flaggedCategory {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Chat_V1_EditChatResponse.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\u{1}TITLE_MODERATED\0\u{1}PICTURE_BLOB_NOT_ACCEPTED\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") diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift index acae12b..318522d 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift @@ -174,7 +174,8 @@ 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. + /// Per-viewer chat state, including what the viewer may do in the chat. + /// Absent when the chat holds nothing about them; always set for a member. public var viewerState: Flipcash_Chat_V1_ViewerState { get {return _storage._viewerState ?? Flipcash_Chat_V1_ViewerState()} set {_uniqueStorage()._viewerState = newValue} @@ -330,43 +331,79 @@ public struct Flipcash_Chat_V1_MinimumBalanceRequirement: Sendable { fileprivate var _amount: Flipcash_Common_V1_FiatPaymentAmount? = nil } -public struct Flipcash_Chat_V1_Member: Sendable { +public struct Flipcash_Chat_V1_Member: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. public var userID: Flipcash_Common_V1_UserId { - get {return _userID ?? Flipcash_Common_V1_UserId()} - set {_userID = newValue} + get {return _storage._userID ?? Flipcash_Common_V1_UserId()} + set {_uniqueStorage()._userID = newValue} } /// Returns true if `userID` has been explicitly set. - public var hasUserID: Bool {return self._userID != nil} + public var hasUserID: Bool {return _storage._userID != nil} /// Clears the value of `userID`. Subsequent reads from it will return its default value. - public mutating func clearUserID() {self._userID = nil} + public mutating func clearUserID() {_uniqueStorage()._userID = nil} /// The user profile for this member. It contains a subset of identifiers /// that can be publicly viewed within the chat. public var userProfile: Flipcash_Profile_V1_UserProfile { - get {return _userProfile ?? Flipcash_Profile_V1_UserProfile()} - set {_userProfile = newValue} + get {return _storage._userProfile ?? Flipcash_Profile_V1_UserProfile()} + set {_uniqueStorage()._userProfile = newValue} } /// Returns true if `userProfile` has been explicitly set. - public var hasUserProfile: Bool {return self._userProfile != nil} + public var hasUserProfile: Bool {return _storage._userProfile != nil} /// Clears the value of `userProfile`. Subsequent reads from it will return its default value. - public mutating func clearUserProfile() {self._userProfile = nil} + public mutating func clearUserProfile() {_uniqueStorage()._userProfile = nil} /// Chat message state for this member. /// /// If set, the list may contain DELIVERED and READ pointers. SENT pointers /// are only shared between the sender and server, to indicate persistence. - public var pointers: [Flipcash_Messaging_V1_Pointer] = [] + /// Hydrated for a DM's participants; a group member's are never carried. + public var pointers: [Flipcash_Messaging_V1_Pointer] { + get {return _storage._pointers} + set {_uniqueStorage()._pointers = newValue} + } + + /// When the member most recently joined the chat. A member who left and + /// rejoined carries the rejoin time. + /// + /// Set for a group's members on a Chat.GetRoster page and on a + /// RosterUpdate.MemberJoined — the carriers a client builds its member + /// list from. Unset on Metadata.members, which carries only the viewer's + /// own entry, and never set for a DM's participants, whose roster is fixed + /// at creation. + public var joinedAt: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _storage._joinedAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_uniqueStorage()._joinedAt = newValue} + } + /// Returns true if `joinedAt` has been explicitly set. + public var hasJoinedAt: Bool {return _storage._joinedAt != nil} + /// Clears the value of `joinedAt`. Subsequent reads from it will return its default value. + public mutating func clearJoinedAt() {_uniqueStorage()._joinedAt = nil} + + /// The roster version at which the member most recently joined: the + /// version RosterSummary.version moved to on that transition, so it equals + /// roster_summary.version on the RosterUpdate.MemberJoined that announced + /// it. Zero for a member joined at the chat's creation and for every DM + /// participant. In future it moves with any change to what the chat + /// records about the member (e.g. a role), as RosterSummary.version does. + /// + /// A client merges what it holds for a user by this version: the greater + /// wins, so a GetRoster page that trails the stream (see GetRoster) + /// cannot resurrect a member the stream has already removed. Set wherever + /// joined_at is; a DM participant's is zero by that rule too. + public var version: UInt64 { + get {return _storage._version} + set {_uniqueStorage()._version = newValue} + } public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} - fileprivate var _userID: Flipcash_Common_V1_UserId? = nil - fileprivate var _userProfile: Flipcash_Profile_V1_UserProfile? = nil + fileprivate var _storage = _StorageClass.defaultInstance } /// RosterSummary describes a chat's roster — its member list — without @@ -382,7 +419,8 @@ public struct Flipcash_Chat_V1_RosterSummary: Sendable { // methods supported on all messages. /// Number of currently joined members. For a large group chat, - /// Metadata.members is a subset of the roster; this is its true size. + /// Metadata.members is a subset of the roster; this is its true size, and + /// Chat.GetRoster pages the roster itself. public var memberCount: UInt64 = 0 /// Version of the roster: advanced by exactly one on every change to the @@ -436,12 +474,30 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { set {kind = .viewerStateChanged(newValue)} } + public var titleChanged: Flipcash_Chat_V1_MetadataUpdate.TitleChanged { + get { + if case .titleChanged(let v)? = kind {return v} + return Flipcash_Chat_V1_MetadataUpdate.TitleChanged() + } + set {kind = .titleChanged(newValue)} + } + + public var pictureChanged: Flipcash_Chat_V1_MetadataUpdate.PictureChanged { + get { + if case .pictureChanged(let v)? = kind {return v} + return Flipcash_Chat_V1_MetadataUpdate.PictureChanged() + } + set {kind = .pictureChanged(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) + case titleChanged(Flipcash_Chat_V1_MetadataUpdate.TitleChanged) + case pictureChanged(Flipcash_Chat_V1_MetadataUpdate.PictureChanged) } @@ -511,6 +567,50 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { fileprivate var _viewerState: Flipcash_Chat_V1_ViewerState? = nil } + /// The chat's title has changed (e.g. via Chat.EditChat). Delivered to the + /// chat's members, including the editor's other devices. Best-effort and + /// applied as received; a client that suspects a miss refetches the chat + /// via Chat.GetChat. + public struct TitleChanged: 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 new title, replacing Metadata.title. + public var newTitle: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + + /// The chat's picture has changed (e.g. via Chat.EditChat). Delivered to + /// the chat's members, including the editor's other devices. Best-effort + /// and applied as received; a client that suspects a miss refetches the + /// chat via Chat.GetChat. + public struct PictureChanged: 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 new picture, with the renditions the server derived, replacing + /// Metadata.picture. + public var newPicture: Flipcash_Blob_V1_Media { + get {return _newPicture ?? Flipcash_Blob_V1_Media()} + set {_newPicture = newValue} + } + /// Returns true if `newPicture` has been explicitly set. + public var hasNewPicture: Bool {return self._newPicture != nil} + /// Clears the value of `newPicture`. Subsequent reads from it will return its default value. + public mutating func clearNewPicture() {self._newPicture = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _newPicture: Flipcash_Blob_V1_Media? = nil + } + public init() {} } @@ -526,42 +626,39 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { /// lesser or equal version should be dropped, so delivery order does not /// matter. A missed update is not caught up via GetDelta but reconciled by /// refetching the roster when a client observes a version it cannot reconcile. -public struct Flipcash_Chat_V1_RosterUpdate: @unchecked Sendable { +public struct Flipcash_Chat_V1_RosterUpdate: 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 kind: OneOf_Kind? { - get {return _storage._kind} - set {_uniqueStorage()._kind = newValue} - } + public var kind: Flipcash_Chat_V1_RosterUpdate.OneOf_Kind? = nil public var memberJoined: Flipcash_Chat_V1_RosterUpdate.MemberJoined { get { - if case .memberJoined(let v)? = _storage._kind {return v} + if case .memberJoined(let v)? = kind {return v} return Flipcash_Chat_V1_RosterUpdate.MemberJoined() } - set {_uniqueStorage()._kind = .memberJoined(newValue)} + set {kind = .memberJoined(newValue)} } public var memberLeft: Flipcash_Chat_V1_RosterUpdate.MemberLeft { get { - if case .memberLeft(let v)? = _storage._kind {return v} + if case .memberLeft(let v)? = kind {return v} return Flipcash_Chat_V1_RosterUpdate.MemberLeft() } - set {_uniqueStorage()._kind = .memberLeft(newValue)} + set {kind = .memberLeft(newValue)} } /// The chat's roster summary after this change, applied by version as /// described above. public var rosterSummary: Flipcash_Chat_V1_RosterSummary { - get {return _storage._rosterSummary ?? Flipcash_Chat_V1_RosterSummary()} - set {_uniqueStorage()._rosterSummary = newValue} + get {return _rosterSummary ?? Flipcash_Chat_V1_RosterSummary()} + set {_rosterSummary = newValue} } /// Returns true if `rosterSummary` has been explicitly set. - public var hasRosterSummary: Bool {return _storage._rosterSummary != nil} + public var hasRosterSummary: Bool {return self._rosterSummary != nil} /// Clears the value of `rosterSummary`. Subsequent reads from it will return its default value. - public mutating func clearRosterSummary() {_uniqueStorage()._rosterSummary = nil} + public mutating func clearRosterSummary() {self._rosterSummary = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -579,8 +676,9 @@ public struct Flipcash_Chat_V1_RosterUpdate: @unchecked Sendable { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// The member that joined, with their profile hydrated, so a client - /// can update its cached member list without a refetch. + /// The member that joined, with their profile hydrated and joined_at and + /// version set, so a client can update its cached member list without + /// a refetch and merge later Chat.GetRoster pages against it. public var member: Flipcash_Chat_V1_Member { get {return _member ?? Flipcash_Chat_V1_Member()} set {_member = newValue} @@ -642,7 +740,7 @@ public struct Flipcash_Chat_V1_RosterUpdate: @unchecked Sendable { public init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _rosterSummary: Flipcash_Chat_V1_RosterSummary? = nil } public struct Flipcash_Chat_V1_RosterUpdateBatch: Sendable { @@ -659,7 +757,8 @@ public struct Flipcash_Chat_V1_RosterUpdateBatch: Sendable { /// 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. +/// shared with other members. Absent when the chat holds nothing about the +/// viewer; always present for a member, whose permissions it carries. 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 @@ -675,6 +774,21 @@ public struct Flipcash_Chat_V1_ViewerState: Sendable { /// Clears the value of `settings`. Subsequent reads from it will return its default value. public mutating func clearSettings() {self._settings = nil} + /// What the viewer may do in this chat. Server-computed from the viewer's + /// standing in the chat and its rules, and never derivable by the client: + /// a client shows an affordance if and only if its flag is set. A change + /// to any flag (e.g. the viewer being granted or losing the ability to + /// edit) advances version and reaches the viewer's devices as a + /// MetadataUpdate.ViewerStateChanged. + public var permissions: Flipcash_Chat_V1_ViewerState.Permissions { + get {return _permissions ?? Flipcash_Chat_V1_ViewerState.Permissions()} + set {_permissions = newValue} + } + /// Returns true if `permissions` has been explicitly set. + public var hasPermissions: Bool {return self._permissions != nil} + /// Clears the value of `permissions`. Subsequent reads from it will return its default value. + public mutating func clearPermissions() {self._permissions = 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. @@ -707,9 +821,28 @@ public struct Flipcash_Chat_V1_ViewerState: Sendable { fileprivate var _mute: Flipcash_Chat_V1_MuteState? = nil } + /// Permissions are the actions the viewer may take in the chat. Each flag + /// is named for the RPC it gates and defaults to false, so an unset flag + /// always means the action is not permitted. + public struct Permissions: 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. + + /// Whether the viewer may call Chat.EditChat on this chat. False for + /// every DM, and for a group member the server does not permit to + /// edit it. + public var canEdit: Bool = false + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} fileprivate var _settings: Flipcash_Chat_V1_ViewerState.Settings? = nil + fileprivate var _permissions: Flipcash_Chat_V1_ViewerState.Permissions? = nil } public struct Flipcash_Chat_V1_MuteState: Sendable { @@ -1166,43 +1299,97 @@ extension Flipcash_Chat_V1_MinimumBalanceRequirement: SwiftProtobuf.Message, Swi extension Flipcash_Chat_V1_Member: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Member" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}user_profile\0\u{1}pointers\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}user_profile\0\u{1}pointers\0\u{3}joined_at\0\u{1}version\0") + + fileprivate class _StorageClass { + var _userID: Flipcash_Common_V1_UserId? = nil + var _userProfile: Flipcash_Profile_V1_UserProfile? = nil + var _pointers: [Flipcash_Messaging_V1_Pointer] = [] + var _joinedAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + var _version: UInt64 = 0 + + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _userID = source._userID + _userProfile = source._userProfile + _pointers = source._pointers + _joinedAt = source._joinedAt + _version = source._version + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } 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._userID) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._userProfile) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &self.pointers) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._userID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._userProfile) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &_storage._pointers) }() + case 4: try { try decoder.decodeSingularMessageField(value: &_storage._joinedAt) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &_storage._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._userID { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._userProfile { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if !self.pointers.isEmpty { - try visitor.visitRepeatedMessageField(value: self.pointers, fieldNumber: 3) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._userID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = _storage._userProfile { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if !_storage._pointers.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._pointers, fieldNumber: 3) + } + try { if let v = _storage._joinedAt { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + if _storage._version != 0 { + try visitor.visitSingularUInt64Field(value: _storage._version, fieldNumber: 5) + } } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Chat_V1_Member, rhs: Flipcash_Chat_V1_Member) -> Bool { - if lhs._userID != rhs._userID {return false} - if lhs._userProfile != rhs._userProfile {return false} - if lhs.pointers != rhs.pointers {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._userID != rhs_storage._userID {return false} + if _storage._userProfile != rhs_storage._userProfile {return false} + if _storage._pointers != rhs_storage._pointers {return false} + if _storage._joinedAt != rhs_storage._joinedAt {return false} + if _storage._version != rhs_storage._version {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1245,7 +1432,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\u{3}viewer_state_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\u{3}title_changed\0\u{3}picture_changed\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1292,6 +1479,32 @@ extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf. self.kind = .viewerStateChanged(v) } }() + case 4: try { + var v: Flipcash_Chat_V1_MetadataUpdate.TitleChanged? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .titleChanged(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .titleChanged(v) + } + }() + case 5: try { + var v: Flipcash_Chat_V1_MetadataUpdate.PictureChanged? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .pictureChanged(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .pictureChanged(v) + } + }() default: break } } @@ -1315,6 +1528,14 @@ extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf. guard case .viewerStateChanged(let v)? = self.kind else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 3) }() + case .titleChanged?: try { + guard case .titleChanged(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .pictureChanged?: try { + guard case .pictureChanged(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1429,111 +1650,137 @@ extension Flipcash_Chat_V1_MetadataUpdate.ViewerStateChanged: SwiftProtobuf.Mess } } -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") - - fileprivate class _StorageClass { - var _kind: Flipcash_Chat_V1_RosterUpdate.OneOf_Kind? - var _rosterSummary: Flipcash_Chat_V1_RosterSummary? = nil +extension Flipcash_Chat_V1_MetadataUpdate.TitleChanged: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_MetadataUpdate.protoMessageName + ".TitleChanged" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}new_title\0") - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _kind = source._kind - _rosterSummary = source._rosterSummary + 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.decodeSingularStringField(value: &self.newTitle) }() + default: break + } } } - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) + public func traverse(visitor: inout V) throws { + if !self.newTitle.isEmpty { + try visitor.visitSingularStringField(value: self.newTitle, fieldNumber: 1) } - return _storage + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MetadataUpdate.TitleChanged, rhs: Flipcash_Chat_V1_MetadataUpdate.TitleChanged) -> Bool { + if lhs.newTitle != rhs.newTitle {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true } +} + +extension Flipcash_Chat_V1_MetadataUpdate.PictureChanged: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_MetadataUpdate.protoMessageName + ".PictureChanged" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}new_picture\0") public mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Flipcash_Chat_V1_RosterUpdate.MemberJoined? - var hadOneofValue = false - if let current = _storage._kind { - hadOneofValue = true - if case .memberJoined(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._kind = .memberJoined(v) - } - }() - case 2: try { - var v: Flipcash_Chat_V1_RosterUpdate.MemberLeft? - var hadOneofValue = false - if let current = _storage._kind { - hadOneofValue = true - if case .memberLeft(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._kind = .memberLeft(v) - } - }() - case 10: try { try decoder.decodeSingularMessageField(value: &_storage._rosterSummary) }() - default: break - } + 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._newPicture) }() + default: break } } } public func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._newPicture { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MetadataUpdate.PictureChanged, rhs: Flipcash_Chat_V1_MetadataUpdate.PictureChanged) -> Bool { + if lhs._newPicture != rhs._newPicture {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") + + 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 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 _storage._kind { - case .memberJoined?: try { - guard case .memberJoined(let v)? = _storage._kind else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + // 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_Chat_V1_RosterUpdate.MemberJoined? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .memberJoined(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .memberJoined(v) + } }() - case .memberLeft?: try { - guard case .memberLeft(let v)? = _storage._kind else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + case 2: try { + var v: Flipcash_Chat_V1_RosterUpdate.MemberLeft? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .memberLeft(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .memberLeft(v) + } }() - case nil: break + case 10: try { try decoder.decodeSingularMessageField(value: &self._rosterSummary) }() + default: break } - try { if let v = _storage._rosterSummary { - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) - } }() } + } + + 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.kind { + case .memberJoined?: try { + guard case .memberJoined(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .memberLeft?: try { + guard case .memberLeft(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try { if let v = self._rosterSummary { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Chat_V1_RosterUpdate, rhs: Flipcash_Chat_V1_RosterUpdate) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._kind != rhs_storage._kind {return false} - if _storage._rosterSummary != rhs_storage._rosterSummary {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs.kind != rhs.kind {return false} + if lhs._rosterSummary != rhs._rosterSummary {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1644,7 +1891,7 @@ 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 static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}settings\0\u{1}permissions\0\u{2}\u{8}version\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1653,6 +1900,7 @@ extension Flipcash_Chat_V1_ViewerState: SwiftProtobuf.Message, SwiftProtobuf._Me // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularMessageField(value: &self._settings) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._permissions) }() case 10: try { try decoder.decodeSingularUInt64Field(value: &self.version) }() default: break } @@ -1667,6 +1915,9 @@ extension Flipcash_Chat_V1_ViewerState: SwiftProtobuf.Message, SwiftProtobuf._Me try { if let v = self._settings { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) } }() + try { if let v = self._permissions { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() if self.version != 0 { try visitor.visitSingularUInt64Field(value: self.version, fieldNumber: 10) } @@ -1675,6 +1926,7 @@ extension Flipcash_Chat_V1_ViewerState: SwiftProtobuf.Message, SwiftProtobuf._Me public static func ==(lhs: Flipcash_Chat_V1_ViewerState, rhs: Flipcash_Chat_V1_ViewerState) -> Bool { if lhs._settings != rhs._settings {return false} + if lhs._permissions != rhs._permissions {return false} if lhs.version != rhs.version {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -1715,6 +1967,36 @@ extension Flipcash_Chat_V1_ViewerState.Settings: SwiftProtobuf.Message, SwiftPro } } +extension Flipcash_Chat_V1_ViewerState.Permissions: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_ViewerState.protoMessageName + ".Permissions" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}can_edit\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.decodeSingularBoolField(value: &self.canEdit) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.canEdit != false { + try visitor.visitSingularBoolField(value: self.canEdit, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_ViewerState.Permissions, rhs: Flipcash_Chat_V1_ViewerState.Permissions) -> Bool { + if lhs.canEdit != rhs.canEdit {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") diff --git a/flipcash2.lock b/flipcash2.lock index ac4a823..7520150 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: dd5e92db6f76700ab6b565f4eff1f9fb25140c9a -subject: Add Reporting RPC service (#113) +commit: 4ccbbe43197ec6cc2d7a3f0681a73f799cdcfb46 +subject: Add EditChat RPC (#116) diff --git a/proto/chat/v1/chat_service.proto b/proto/chat/v1/chat_service.proto index b4cfd2b..c532205 100644 --- a/proto/chat/v1/chat_service.proto +++ b/proto/chat/v1/chat_service.proto @@ -65,6 +65,35 @@ service Chat { // pages is dropped, and its removal arrives on the stream. rpc GetGroupChatFeed(GetGroupChatFeedRequest) returns (GetGroupChatFeedResponse); + // GetRoster pages a chat's roster, most recently joined first. Every + // page carries the chat's RosterSummary, and every member carries the + // roster version that placed them (see Member.version). + // + // A DM's roster is its participants, and a small group's is read whole: + // for these a page is the roster at exactly roster_summary.version — a + // roster that fits one page is member_count members, and a later page + // is a fresh snapshot at its own summary. A large group's roster is + // paged from an index that trails membership writes briefly, so a page + // may lag roster_summary — a member + // who just joined may be absent, one who just left may be present. + // Clients do not see which case they are in and must follow the weaker + // contract: treat roster_summary.version as the staleness watermark + // described on RosterSummary, and merge each page against what the + // event stream has told them per member by Member.version, the greater + // winning. A cached member absent from a fully read roster is gone + // unless the client holds their join at a version above the page's + // roster_summary.version. A join during the walk lands ahead of the + // cursor and arrives only as a RosterUpdate. + // + // Pointers are hydrated for a DM's participants only. A group's members + // carry none: group pointer advances are never broadcast, so a page of + // them would be stale as soon as it was served. + // + // Requires that the caller may read the chat: a member, or a non-member + // a group's listener rules admit. A viewer who may only preview the chat + // is DENIED. + rpc GetRoster(GetRosterRequest) returns (GetRosterResponse); + // StartChat starts a new chat. rpc StartChat(StartChatRequest) returns (StartChatResponse); @@ -74,6 +103,25 @@ service Chat { // LeaveChat removes the caller from a chat's roster. rpc LeaveChat(LeaveChatRequest) returns (LeaveChatResponse); + // EditChat edits a group chat's record. Every editable field is optional; + // only the ones set in the request are changed, and the edit is atomic: + // if any part is refused, nothing is applied. + // + // Only a group chat may be edited, and only by a member the server permits + // to edit it, as reported by ViewerState.Permissions.can_edit; anyone else + // is DENIED. A new title is moderated like + // StartChat's. A new picture is a blob the caller has already uploaded via + // BlobStorage: the client uploads only the ORIGINAL and passes the + // resulting BlobId once the blob is READY, and the server derives the + // remaining renditions. Setting a field to the value the chat already has + // is a no-op for that field, and a request that sets nothing is a no-op + // that returns OK. + // + // Every real change reaches the chat's members, including the caller's + // other devices, on the event stream as one MetadataUpdate per field + // changed: TitleChanged for the title, PictureChanged for the picture. + rpc EditChat(EditChatRequest) returns (EditChatResponse); + // 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 @@ -216,6 +264,56 @@ message GetGroupChatFeedResponse { bool has_more = 4; } +message GetRosterRequest { + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + // QueryOptions controls page_size, capped at 100. Ordering is fixed to + // most recently joined first and is not client-selectable. + // + // Leave query_options.paging_token unset on the first request. On every + // subsequent request, set it to the paging_token from the most recent + // response. The token is opaque and server-generated; do not construct + // it. It is bound to chat_id and refused with another chat. + common.v1.QueryOptions query_options = 2; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message GetRosterResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + NOT_FOUND = 2; + } + + // The page, most recently joined first, each with user_profile, + // joined_at and version set, and pointers for a DM. Set when result is + // OK. + repeated Member members = 2 [(validate.rules).repeated = { + min_items: 0 + max_items: 100 + }]; + + // The chat's roster summary as of this page's read. Set when result is + // OK. Applied by version as described on RosterSummary; see GetRoster + // for how the page relates to it. + RosterSummary roster_summary = 3; + + // PagingToken carries the cursor over (joined_at, user_id) after the last + // member returned. The client MUST send it back in + // query_options.paging_token on the next GetRosterRequest. Set when + // result is OK and has_more is true. + common.v1.PagingToken paging_token = 4; + + // HasMore indicates whether further pages remain. When false, the roster + // has been fully read as of this page; the complete member list is this + // set reconciled with the event stream (see GetRoster). When true, the + // client should issue another GetRosterRequest with the returned + // paging_token. + bool has_more = 5; +} + message StartChatRequest { // Parameters for the kind of chat being created. The variant selects the // chat type. @@ -308,6 +406,53 @@ message LeaveChatResponse { } } +message EditChatRequest { + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + // The new title. Left unchanged when unset. + Title title = 2; + + // The new picture. Left unchanged when unset. + Picture picture = 3; + + message Title { + string value = 1 [(validate.rules).string = { + min_len: 1 + max_len: 64 + }]; + } + + message Picture { + // The blob holding the ORIGINAL picture the caller uploaded. It must be + // owned by the caller and READY; the server derives the remaining + // renditions from it. + blob.v1.BlobId blob_id = 1 [(validate.rules).message.required = true]; + } + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message EditChatResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + NOT_FOUND = 2; + TITLE_MODERATED = 3; + PICTURE_BLOB_NOT_ACCEPTED = 4; + } + + // The metadata for the chat after the edit, as seen by the caller, + // including any picture renditions the server derived. Set only when + // result == OK. + Metadata chat = 2; + + // The best-fit category that tripped moderation, mirroring the Moderation + // service's vocabulary. Set only when result == TITLE_MODERATED; NONE + // otherwise. + moderation.v1.FlaggedCategory flagged_category = 3; +} + message MuteChatRequest { common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; diff --git a/proto/chat/v1/model.proto b/proto/chat/v1/model.proto index c13eb3e..5e52d1d 100644 --- a/proto/chat/v1/model.proto +++ b/proto/chat/v1/model.proto @@ -75,7 +75,8 @@ message Metadata { // 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. + // Per-viewer chat state, including what the viewer may do in the chat. + // Absent when the chat holds nothing about them; always set for a member. ViewerState viewer_state = 12; } @@ -156,10 +157,34 @@ message Member { // // If set, the list may contain DELIVERED and READ pointers. SENT pointers // are only shared between the sender and server, to indicate persistence. + // Hydrated for a DM's participants; a group member's are never carried. repeated messaging.v1.Pointer pointers = 3 [(validate.rules).repeated = { min_items: 0 max_items: 2 }]; + + // When the member most recently joined the chat. A member who left and + // rejoined carries the rejoin time. + // + // Set for a group's members on a Chat.GetRoster page and on a + // RosterUpdate.MemberJoined — the carriers a client builds its member + // list from. Unset on Metadata.members, which carries only the viewer's + // own entry, and never set for a DM's participants, whose roster is fixed + // at creation. + google.protobuf.Timestamp joined_at = 4; + + // The roster version at which the member most recently joined: the + // version RosterSummary.version moved to on that transition, so it equals + // roster_summary.version on the RosterUpdate.MemberJoined that announced + // it. Zero for a member joined at the chat's creation and for every DM + // participant. In future it moves with any change to what the chat + // records about the member (e.g. a role), as RosterSummary.version does. + // + // A client merges what it holds for a user by this version: the greater + // wins, so a GetRoster page that trails the stream (see GetRoster) + // cannot resurrect a member the stream has already removed. Set wherever + // joined_at is; a DM participant's is zero by that rule too. + uint64 version = 5; } // RosterSummary describes a chat's roster — its member list — without @@ -171,7 +196,8 @@ message Member { // this summary. message RosterSummary { // Number of currently joined members. For a large group chat, - // Metadata.members is a subset of the roster; this is its true size. + // Metadata.members is a subset of the roster; this is its true size, and + // Chat.GetRoster pages the roster itself. uint64 member_count = 1; // Version of the roster: advanced by exactly one on every change to the @@ -197,6 +223,8 @@ message MetadataUpdate { FullRefresh full_refresh = 1; LastActivityChanged last_activity_changed = 2; ViewerStateChanged viewer_state_changed = 3; + TitleChanged title_changed = 4; + PictureChanged picture_changed = 5; } // Refreshes the entire chat metadata @@ -213,6 +241,28 @@ message MetadataUpdate { message ViewerStateChanged { ViewerState viewer_state = 1 [(validate.rules).message.required = true]; } + + // The chat's title has changed (e.g. via Chat.EditChat). Delivered to the + // chat's members, including the editor's other devices. Best-effort and + // applied as received; a client that suspects a miss refetches the chat + // via Chat.GetChat. + message TitleChanged { + // The new title, replacing Metadata.title. + string new_title = 1 [(validate.rules).string = { + min_len: 1 + max_len: 64 + }]; + } + + // The chat's picture has changed (e.g. via Chat.EditChat). Delivered to + // the chat's members, including the editor's other devices. Best-effort + // and applied as received; a client that suspects a miss refetches the + // chat via Chat.GetChat. + message PictureChanged { + // The new picture, with the renditions the server derived, replacing + // Metadata.picture. + blob.v1.Media new_picture = 1 [(validate.rules).message.required = true]; + } } // RosterUpdate is a best-effort, real-time change to a chat's roster — a @@ -243,8 +293,9 @@ message RosterUpdate { // has just become a member of the chat and should insert it into their // chat list using metadata. message MemberJoined { - // The member that joined, with their profile hydrated, so a client - // can update its cached member list without a refetch. + // The member that joined, with their profile hydrated and joined_at and + // version set, so a client can update its cached member list without + // a refetch and merge later Chat.GetRoster pages against it. Member member = 1 [(validate.rules).message.required = true]; // The full metadata for the chat, so the recipient can insert it into @@ -276,11 +327,20 @@ 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. +// shared with other members. Absent when the chat holds nothing about the +// viewer; always present for a member, whose permissions it carries. message ViewerState { // State the viewer set for themselves through their own RPCs. Settings settings = 1; + // What the viewer may do in this chat. Server-computed from the viewer's + // standing in the chat and its rules, and never derivable by the client: + // a client shows an affordance if and only if its flag is set. A change + // to any flag (e.g. the viewer being granted or losing the ability to + // edit) advances version and reaches the viewer's devices as a + // MetadataUpdate.ViewerStateChanged. + Permissions permissions = 2; + // 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. @@ -293,6 +353,16 @@ message ViewerState { // client suppresses the notification. MuteState mute = 1; } + + // Permissions are the actions the viewer may take in the chat. Each flag + // is named for the RPC it gates and defaults to false, so an unset flag + // always means the action is not permitted. + message Permissions { + // Whether the viewer may call Chat.EditChat on this chat. False for + // every DM, and for a group member the server does not permit to + // edit it. + bool can_edit = 1; + } } message MuteState {