Unverified Commit 24fc8ab8 authored by memoclaw's avatar memoclaw Committed by GitHub

feat(mentions): add memo mention parsing, notifications, and rendering (#5811)

Co-authored-by: 's avatarmemoclaw <265580040+memoclaw@users.noreply.github.com>
parent 38fc22b7
## Background & Context
Memos stores memo bodies as markdown, rebuilds derived memo metadata into `MemoPayload`, exposes user notifications through the inbox model, and renders memo content in the React client with custom markdown plugins. The requested `@someone` feature spans both top-level memos and memo comments: users need to type `@`, pick a valid person, render the mention inline, and notify the mentioned user. The current product already has adjacent primitives for this work: a backend markdown extension for `#tag`, an inbox-backed notification center, a generic editor suggestion popup, public user profiles under username-based routes, and a memo update path that already rebuilds payloads on create and edit.
External product behavior is consistent on the core interaction but different on scope. Notion supports real-time `@` suggestions inside pages, comments, and discussions, stores mention notifications in an inbox, and suppresses notification if the mentioned user cannot access the content. Confluence supports autocomplete mentions for people and teams, sends a notification on the first mention, and does not keep notifying on repeated mentions in the same page. Coda supports `@` mentions inside comment threads, treats mentions and thread participation as notification triggers, and allows broader comment-subscription settings beyond explicit mentions. These patterns suggest that the common baseline for Memos is inline autocomplete, access-aware notification, deduplication, and a clear separation between mention notifications and broader thread-subscription features.
## Issue Statement
Memos does not currently recognize `@username` tokens as structured content in memo bodies or comment bodies, does not expose any non-admin user-search endpoint that the editor can use to suggest mentionable users, does not persist or diff mention metadata during memo create or update flows, and does not have an inbox or API notification type for mentions. As a result, `@someone` currently behaves as plain text and cannot drive inline rendering, target validation, or notification delivery.
## Current State
- `server/router/api/v1/memo_service.go:32-159` creates memos by copying raw `request.Memo.Content` into `store.Memo`, enforcing length limits, and calling `memopayload.RebuildMemoPayload`; `server/router/api/v1/memo_service.go:404-510` rebuilds payload only when `content` changes during memo updates.
- `server/router/api/v1/memo_service.go:590-681` creates memo comments by internally creating another memo and only generates inbox notifications for non-private comments to the parent memo creator via `InboxMessage_MEMO_COMMENT`.
- `server/router/api/v1/memo_update_helpers.go:27-77` only dispatches webhook and SSE side effects after memo updates; there is no mention-diff side-effect hook.
- `internal/markdown/markdown.go:20-24` defines extracted markdown metadata as `Tags` plus `Property`; `internal/markdown/markdown.go:68-89` only wires the custom tag extension; `internal/markdown/markdown.go:324-386` extracts tags and properties but no mention metadata.
- `internal/markdown/extensions/tag.go:13-23` and the related tag parser/AST types are the only custom inline markdown extension path today.
- `proto/store/memo.proto:7-29` limits `MemoPayload` to `property`, `location`, and `tags`; there is no repeated mention field or structured mention metadata.
- `proto/store/inbox.proto:7-24` defines only `InboxMessage_MEMO_COMMENT`; `proto/api/v1/user_service.proto:592-679` defines only `UserNotification_MEMO_COMMENT`.
- `server/router/api/v1/user_service.go:1272-1312` lists notifications by filtering inbox rows to `InboxMessage_MEMO_COMMENT` only; `server/router/api/v1/user_service.go:1433-1524` converts only that message type into API notifications.
- `web/src/pages/Inboxes.tsx:19-114` and `web/src/components/Inbox/MemoCommentMessage.tsx` only render memo comment notifications; other notification types would currently be dropped.
- `server/router/api/v1/user_service.go:32-70` exposes `ListUsers` only to admins, and `store/user.go:59-74` plus `store/db/sqlite/user.go:88-175` support exact-match user filtering but no general search, ranking, or pagination for mention autocomplete.
- `server/router/api/v1/acl_config.go:20-27` whitelists `/memos.api.v1.UserService/SearchUsers`, but `proto/api/v1/user_service.proto:16-120` does not define a `SearchUsers` RPC and there is no server implementation.
- `web/src/components/MemoEditor/Editor/index.tsx:189-214`, `web/src/components/MemoEditor/Editor/useSuggestions.ts:28-158`, and `web/src/components/MemoEditor/Editor/TagSuggestions.tsx:10-49` provide a reusable textarea suggestion popup, but it is only instantiated for `#tag`.
- `web/src/components/MemoContent/index.tsx:53-136`, `web/src/utils/remark-plugins/remark-tag.ts:24-112`, and `web/src/components/MemoContent/Tag.tsx` parse and render `#tag` as a structured inline element; there is no `remarkMention` equivalent.
- `web/src/hooks/useUserQueries.ts:176-245` has `useListUsers()` for admin listing and `useUsersByNames()` for fetching known usernames one by one, but nothing that returns ranked candidates for an in-editor `@` query.
- `web/src/router/index.tsx:65-72` already routes public user profiles at `u/:username`, so inline mention rendering can target username-based profile URLs without inventing a new frontend route.
## Non-Goals
- Adding group mentions, team mentions, page mentions, or date mentions.
- Building a general “watch this memo/thread” subscription system beyond explicit mentions.
- Adding email, push, Slack, or webhook delivery for mentions in this issue.
- Redesigning memo visibility, access control, or per-user sharing semantics.
- Making old mentions follow username changes automatically.
- Redesigning the editor away from the current textarea-based implementation.
## Open Questions
- Which content surfaces are in scope for `@mention`? (default: top-level memos and memo comments, because both already share the same memo content pipeline)
- What mention token syntax should be recognized? (default: `@username` only, using canonical usernames rather than display names)
- Should edits trigger mention notifications after the initial create? (default: yes, but only for newly added mention targets compared with the memo’s previous mention set)
- What happens if someone types `@username` in content the target cannot access? (default: render the token as a mention in the author’s view, but do not send a notification unless the target can already access the memo/comment under existing visibility rules)
- Should mentioning yourself create an inbox item? (default: no, because self-mentions do not require attention routing)
- Should the mention candidate API be public like `GetUser`, or authenticated like the editor? (default: authenticated only, because ranked user search is a broader directory-enumeration surface than fetching a known public profile)
## Scope
**L** — The work crosses markdown parsing, memo payload extraction, memo create/update side effects, inbox and notification protos, user search APIs, three SQL drivers, React editor autocomplete, markdown rendering, and inbox UI. The repository already contains adjacent pieces for tags and comment notifications, but `@mention` requires stitching several existing subsystems together rather than extending a single isolated module.
## References
- [Comments, mentions & reactions - Notion Help Center](https://www.notion.com/help/comments-mentions-and-reminders)
- [Notification settings - Notion Help Center](https://www.notion.com/help/notification-settings)
- [Mention a person or team - Confluence Cloud](https://support.atlassian.com/confluence-cloud/docs/mention-a-person-or-team/)
- [Comment on Coda docs - Coda Help](https://help.coda.io/hc/en-us/articles/39555917053069-Comment-on-Coda-docs)
- [Customize notifications from comments - Coda Help](https://help.coda.io/hc/en-us/articles/39555901119117-Customize-notifications-from-comments)
## Industry Baseline
`Comments, mentions & reactions - Notion Help Center` shows the most common editor-side behavior: typing `@` triggers real-time search, mentions can live inline in page bodies and comments, clicking an inbox item takes the user back to the exact context, and no notification is sent when the target cannot access the page. `Notification settings - Notion Help Center` also separates in-product inbox behavior from secondary delivery like desktop or email.
`Mention a person or team - Confluence Cloud` adds two useful guardrails for a collaborative editor: autocomplete suggestions appear directly from `@`, and notifications are intentionally deduplicated so people are notified on the first mention rather than on every repeated mention in the same page.
`Comment on Coda docs` and `Customize notifications from comments` show a narrower scope for mentions inside comments, but reinforce two patterns that matter for Memos: explicit `@` mentions are a distinct notification trigger from generic participation, and products often keep mention notifications separate from broader thread-subscription or owner-subscription rules.
Across these products, the default implementation is not “parse arbitrary display text and hope it matches a user.” The stable interaction is: search among valid workspace members, insert a canonical mention token, render it differently from plain text, and only notify when access and deduplication rules say the event is meaningful.
## Research Summary
Memos already has the right extension points to adopt that baseline without a storage redesign. The backend has a custom inline markdown extension pipeline for `#tag`, memo create and update both rebuild `MemoPayload`, and the inbox model already represents user-facing attention items. The frontend editor already has a trigger-character suggestion popup, the markdown renderer already recognizes custom inline nodes, and public user profiles are already routed by username.
The biggest mismatch is user discovery. The current `ListUsers` path is admin-only and exact-match oriented, while mention autocomplete needs a normal authenticated user search API that can return ranked candidates by username and display name. The second mismatch is notification shape: the inbox and API layers only understand memo-comment notifications today, so a mention feature cannot be expressed as a first-class notification without extending the inbox proto and inbox UI.
Research also suggests that Memos should stay narrower than Notion or Confluence. There is no existing concept of teams, group mentions, page mentions, or per-page ACLs. The codebase already treats usernames as the public user token and memo visibility as a coarse `PUBLIC/PROTECTED/PRIVATE` rule. The best fit is therefore person mentions only, keyed by canonical username, with notification rules that are access-aware and deduplicated across repeated edits.
## Design Goals
- Typing `@` in the memo editor or comment editor shows ranked, authenticated user candidates and inserts a canonical `@username` token on selection.
- The backend extracts mention targets from memo/comment content during create, update, and payload rebuild, and produces the same mention set for equivalent content across all supported databases.
- Mention notifications are created only for newly added targets, at most once per target per memo revision, and never for self-mentions or inaccessible private content.
- Memo content renders resolved mentions as interactive inline entities and degrades unresolved tokens to plain text.
- The inbox API and inbox UI expose mention notifications as a first-class type distinct from comment notifications.
- The design does not require a relational schema migration; it only extends existing proto-backed JSON payloads and server/frontend code paths.
## Non-Goals
- Adding group mentions, team mentions, page mentions, or date mentions.
- Building a generic watch/subscription system for memo activity.
- Sending mention notifications through email, push, Slack, or webhooks.
- Making mention references survive username changes automatically.
- Replacing the textarea editor with a richer block editor.
- Redesigning memo visibility or introducing user-level memo sharing.
## Proposed Design
Support only canonical `@username` mentions in this issue. The parser should recognize the same username token vocabulary that the API already accepts for public user names, instead of trying to match display names or arbitrary free text. This keeps mention authoring aligned with existing user resource naming and avoids ambiguous matches when multiple users share similar display names. Mention suggestions may show both display name and username, but the inserted source text remains `@username`.
Add a backend markdown mention extension parallel to the existing tag extension. Introduce `internal/markdown/ast.MentionNode`, `internal/markdown/parser.NewMentionParser()`, and `internal/markdown/extensions.MentionExtension`, then wire it into `internal/markdown/markdown.go` next to `TagExtension`. The mention parser should require a word boundary before `@` so email addresses and URLs do not become mentions, and it should normalize the captured token to lowercase before lookup because usernames are canonicalized that way in the API layer.
Extend `storepb.MemoPayload` with a repeated mention metadata field, for example `repeated Mention mentions`, where each item stores at least `username` and resolved `user_id`. The raw markdown remains the source of truth for author-visible text, but the payload becomes the normalized server-side mention set for diffing and notification decisions. This reuses the existing memo payload rebuild path and avoids reparsing memo bodies in multiple side-effect handlers. No SQL migration is required because memo payloads are already stored as proto-backed JSON blobs in each database driver.
Teach `memopayload.RebuildMemoPayload` to resolve mention metadata while rebuilding tags and properties. The extraction step should walk the markdown AST once, collect raw `@username` tokens, resolve them to active users via the store, deduplicate by `user_id`, and populate `memo.Payload.Mentions`. Unresolved usernames should not fail memo creation; they should simply be omitted from normalized mention metadata so the feature remains tolerant of free-typed text. This mirrors how the frontend can degrade unresolved tokens back to plain text.
Add a dedicated mention side-effect helper around memo create and update flows. On create, after the memo is persisted and the final payload is available, compute the normalized mentioned user set from `memo.Payload.Mentions` and create inbox items for allowed targets. On update, diff the previous and new normalized mention sets and only notify targets that were newly added in the latest saved revision. This follows the Confluence-style deduplication pattern and prevents repeated notifications when a memo is edited without changing its mention set. If a mention is removed and later re-added, it counts as newly added again and may generate a fresh inbox item.
Apply access and duplication rules before writing inbox rows. Self-mentions are ignored. For top-level memos, notify only when the target can already read the memo under current visibility rules. For comments, notify the mentioned user when they can read the comment context and are not already covered by the existing memo-comment notification to the parent memo owner for that same event. This keeps mention notifications meaningful and avoids sending an owner both a comment notification and a mention notification for the same comment creation unless future product requirements explicitly want both. For `PRIVATE` memos and `PRIVATE` comments, mentions remain author-visible text but do not generate inbox notifications for other users.
Extend inbox storage and API notifications with a dedicated mention type instead of overloading the existing comment type. Add `MEMO_MENTION` to `proto/store/inbox.proto` with a payload that can represent both top-level memos and comments, such as `memo_id` plus optional `related_memo_id`. Mirror that in `proto/api/v1/user_service.proto` with `UserNotification_MEMO_MENTION` and `MemoMentionPayload`. Reuse the current notification conversion pattern in `server/router/api/v1/user_service.go`: resolve memo names from stored IDs, return a first-class mention payload, and let the inbox page render a separate mention card component. This keeps the notification center composable as new activity types appear.
Add an authenticated user-search endpoint specifically for mention autocomplete. The repository already has a stale public-method placeholder for `SearchUsers`, but no proto or handler. Define `SearchUsers` in `proto/api/v1/user_service.proto`, remove it from the public ACL list, and implement it in `server/router/api/v1/user_service.go` as an authenticated RPC that accepts a short query string plus page size. Extend `store.FindUser` with search-oriented fields and implement driver-specific case-insensitive matching in SQLite, MySQL, and PostgreSQL over `username` and `nickname`, ordered by exact username match, username prefix, nickname prefix, then a stable fallback. This produces a usable editor candidate list without reusing the admin-only `ListUsers` contract.
Implement frontend mention suggestions by reusing the existing generic textarea suggestion system. Add a `MentionSuggestions` component beside `TagSuggestions`, hook it into `web/src/components/MemoEditor/Editor/index.tsx`, and back it with a debounced `useSearchUsers(query)` hook. The popup should render avatar, display name, and `@username`, while selection inserts `@username ` exactly. Because `useSuggestions` currently operates on local item arrays, it can stay generic if the mention hook owns the remote query and passes the current ranked results down as `items`.
Implement frontend mention rendering with a dedicated markdown plugin and component instead of trying to infer mentions from links or plain spans. Add `remarkMention` beside `remarkTag`, a `Mention` inline component beside `Tag`, and a mention type guard in `web/src/types/markdown.ts`. The renderer should link resolved mentions to `/u/:username`, show display name or username with avatar-based affordance when lookup data is available, and render unresolved mention text non-interactively. To avoid N-per-mention network fetches, `MemoContent` should collect mentioned usernames from content and hydrate them through the existing `useUsersByNames()` hook once per memo render tree.
Render mention notifications as their own inbox card. Reuse the existing `MemoCommentMessage` pattern, but resolve the source memo/comment and optional related memo from the `MemoMentionPayload`. The card should show who mentioned the user, in what memo or comment, a short snippet, and navigate to the relevant memo detail on click. `web/src/pages/Inboxes.tsx` should switch on both `MEMO_COMMENT` and `MEMO_MENTION` so the inbox can grow by type without silently discarding new notifications.
Do not solve username drift in this issue. If a user later changes username, existing raw markdown still contains the old `@username` text, and rebuilt payload metadata will stop resolving unless the old token still matches a live username. This is acceptable for the current scope because username-history and alias resolution are already out of scope elsewhere in the codebase. The alternative of storing opaque mention IDs in source markdown or adding a username-alias subsystem was rejected because it turns a contained collaboration feature into a broader identity migration project.
## Execution Log
### T1: Add backend mention parsing and payload extraction
**Status**: Completed
**Files Changed**: `internal/markdown/ast/mention.go`, `internal/markdown/parser/mention.go`, `internal/markdown/extensions/mention.go`, `internal/markdown/markdown.go`, `internal/markdown/renderer/markdown_renderer.go`, `server/runner/memopayload/runner.go`, `server/router/api/v1/memo_service.go`, `server/router/api/v1/v1.go`, `server/router/api/v1/test/test_helper.go`, `internal/markdown/markdown_test.go`
**Validation**: `go test ./internal/markdown` — PASS
**Path Corrections**: `RebuildMemoPayload` needed `context + store` so mention resolution could happen during payload rebuild.
**Deviations**: None
### T2: Add mention notifications and user search APIs
**Status**: Completed
**Files Changed**: `proto/store/memo.proto`, `proto/store/inbox.proto`, `proto/api/v1/user_service.proto`, `server/router/api/v1/user_service.go`, `server/router/api/v1/connect_services.go`, `server/router/api/v1/acl_config.go`, `server/router/api/v1/acl_config_test.go`, `server/router/api/v1/memo_mention_helpers.go`, `store/user.go`, `store/db/sqlite/user.go`, `store/db/postgres/user.go`, `store/db/mysql/user.go`, `server/router/api/v1/test/user_notification_test.go`, `server/router/api/v1/test/user_search_test.go`
**Validation**: `go test ./server/router/api/v1/...` — PASS
**Path Corrections**: Unknown legacy inbox message types are filtered server-side to keep unread counts aligned with rendered cards.
**Deviations**: None
### T3: Add frontend mention autocomplete, rendering, and inbox UI
**Status**: Completed
**Files Changed**: `web/src/components/MemoEditor/Editor/MentionSuggestions.tsx`, `web/src/components/MemoEditor/Editor/index.tsx`, `web/src/components/MemoEditor/Editor/useSuggestions.ts`, `web/src/hooks/useUserQueries.ts`, `web/src/utils/remark-plugins/remark-mention.ts`, `web/src/components/MemoContent/MentionContext.tsx`, `web/src/components/MemoContent/Mention.tsx`, `web/src/components/MemoContent/index.tsx`, `web/src/components/MemoContent/ConditionalComponent.tsx`, `web/src/types/markdown.ts`, `web/src/components/Inbox/MemoMentionMessage.tsx`, `web/src/pages/Inboxes.tsx`
**Validation**: `pnpm lint && pnpm build` — PASS
**Path Corrections**: Editor autocomplete reused the existing generic suggestion hook by exposing the live query rather than duplicating keyboard navigation logic.
**Deviations**: None
### T4: Regenerate code and validate the feature
**Status**: Completed
**Files Changed**: `proto/gen/**`, `web/src/types/proto/**`
**Validation**: `buf generate` — PASS; `go test ./internal/markdown ./server/router/api/v1/...` — PASS; `pnpm lint` — PASS; `pnpm build` — PASS
**Path Corrections**: None
**Deviations**: None
## Completion Declaration
All tasks completed successfully
## Task List
### Task Index
T1: Add backend mention parsing and payload extraction [M] — T2: Add mention notifications and user search APIs [L] — T3: Add frontend mention autocomplete, rendering, and inbox UI [L] — T4: Regenerate code and validate the feature [M]
### T1: Add backend mention parsing and payload extraction [M]
**Objective**: Parse `@username` tokens into structured mention metadata during memo payload rebuilds.
**Size**: M
**Files**:
- Create: `internal/markdown/ast/mention.go`
- Create: `internal/markdown/parser/mention.go`
- Create: `internal/markdown/extensions/mention.go`
- Modify: `internal/markdown/markdown.go`
- Modify: `internal/markdown/renderer/markdown_renderer.go`
- Modify: `server/runner/memopayload/runner.go`
- Modify: `server/router/api/v1/memo_service.go`
- Test: `internal/markdown/markdown_test.go`
**Implementation**:
1. Add mention AST/parser/extension parallel to the existing tag implementation.
2. Extend extracted markdown data and `MemoPayload` rebuild to collect normalized mentions and resolve them to users.
3. Update memo create/update and background payload rebuild paths to use the new mention-aware payload builder.
**Boundaries**: Do not add a relational schema migration.
**Dependencies**: None
**Expected Outcome**: Memo payloads carry normalized mention metadata rebuilt from markdown content.
**Validation**: `go test ./internal/markdown` — expected `ok`
### T2: Add mention notifications and user search APIs [L]
**Objective**: Expose mention-aware APIs and create inbox items for newly added mentions.
**Size**: L
**Files**:
- Modify: `proto/store/memo.proto`
- Modify: `proto/store/inbox.proto`
- Modify: `proto/api/v1/user_service.proto`
- Modify: `server/router/api/v1/user_service.go`
- Modify: `server/router/api/v1/connect_services.go`
- Modify: `server/router/api/v1/acl_config.go`
- Modify: `server/router/api/v1/acl_config_test.go`
- Create: `server/router/api/v1/memo_mention_helpers.go`
- Modify: `store/user.go`
- Modify: `store/db/sqlite/user.go`
- Modify: `store/db/postgres/user.go`
- Modify: `store/db/mysql/user.go`
- Test: `server/router/api/v1/test/user_notification_test.go`
- Test: `server/router/api/v1/test/user_search_test.go`
**Implementation**:
1. Extend proto contracts with `MemoPayload.mentions`, `InboxMessage.MEMO_MENTION`, `UserNotification.MEMO_MENTION`, and `SearchUsers`.
2. Implement authenticated user search over username and nickname.
3. Add mention notification side effects for memo create/update/comment flows with diffing and duplicate suppression.
4. Convert inbox rows into either comment or mention notifications and filter unknown legacy types.
**Boundaries**: Do not add email/push/webhook mention delivery.
**Dependencies**: T1
**Expected Outcome**: Mentioned users receive inbox notifications and the editor has an API to fetch mention candidates.
**Validation**: `go test ./server/router/api/v1/...` — expected `ok`
### T3: Add frontend mention autocomplete, rendering, and inbox UI [L]
**Objective**: Let users insert mentions from the editor and render/read them in the UI.
**Size**: L
**Files**:
- Create: `web/src/components/MemoEditor/Editor/MentionSuggestions.tsx`
- Modify: `web/src/components/MemoEditor/Editor/index.tsx`
- Modify: `web/src/components/MemoEditor/Editor/useSuggestions.ts`
- Modify: `web/src/hooks/useUserQueries.ts`
- Create: `web/src/utils/remark-plugins/remark-mention.ts`
- Create: `web/src/components/MemoContent/MentionContext.tsx`
- Create: `web/src/components/MemoContent/Mention.tsx`
- Modify: `web/src/components/MemoContent/index.tsx`
- Modify: `web/src/components/MemoContent/ConditionalComponent.tsx`
- Modify: `web/src/types/markdown.ts`
- Create: `web/src/components/Inbox/MemoMentionMessage.tsx`
- Modify: `web/src/pages/Inboxes.tsx`
**Implementation**:
1. Add `@` autocomplete backed by `SearchUsers`.
2. Add markdown mention parsing/rendering and hydrate mentioned users once per memo render.
3. Add a dedicated inbox card for memo mention notifications.
**Boundaries**: Do not redesign the textarea editor.
**Dependencies**: T2
**Expected Outcome**: Users can insert, see, and open mentions from memo content and inbox notifications.
**Validation**: `pnpm lint && pnpm build` — expected success
### T4: Regenerate code and validate the feature [M]
**Objective**: Regenerate generated code and verify backend/frontend behavior.
**Size**: M
**Files**:
- Modify: `proto/gen/**`
- Modify: `web/src/types/proto/**`
**Implementation**:
1. Run `buf generate` after proto changes.
2. Re-run focused Go tests and frontend lint/build.
**Boundaries**: Do not broaden into unrelated CI cleanup.
**Dependencies**: T1, T2, T3
**Expected Outcome**: Generated code matches the new APIs and validations pass.
**Validation**: `buf generate`, `go test ./internal/markdown ./server/router/api/v1/...`, `pnpm lint`, `pnpm build`
## Out-of-Scope Tasks
- Group/team mentions
- Username alias migration
- Email or push delivery for mentions
- Watch/subscription semantics beyond explicit mentions
package ast
import (
gast "github.com/yuin/goldmark/ast"
)
// MentionNode represents an @mention in the markdown AST.
type MentionNode struct {
gast.BaseInline
// Username without the @ prefix.
Username []byte
}
// KindMention is the NodeKind for MentionNode.
var KindMention = gast.NewNodeKind("Mention")
// Kind returns KindMention.
func (*MentionNode) Kind() gast.NodeKind {
return KindMention
}
// Dump implements Node.Dump for debugging.
func (n *MentionNode) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, map[string]string{
"Username": string(n.Username),
}, nil)
}
package extensions
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/util"
mparser "github.com/usememos/memos/internal/markdown/parser"
)
type mentionExtension struct{}
// MentionExtension is a goldmark extension for @mention syntax.
var MentionExtension = &mentionExtension{}
// Extend extends the goldmark parser with mention support.
func (*mentionExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
// Priority 200 - run before standard link parser (500).
util.Prioritized(mparser.NewMentionParser(), 200),
),
)
}
......@@ -20,6 +20,7 @@ import (
// ExtractedData contains all metadata extracted from markdown in a single pass.
type ExtractedData struct {
Tags []string
Mentions []string
Property *storepb.MemoPayload_Property
}
......@@ -63,6 +64,7 @@ type Option func(*config)
type config struct {
enableTags bool
enableMentions bool
}
// WithTagExtension enables #tag parsing.
......@@ -72,6 +74,13 @@ func WithTagExtension() Option {
}
}
// WithMentionExtension enables @mention parsing.
func WithMentionExtension() Option {
return func(c *config) {
c.enableMentions = true
}
}
// NewService creates a new markdown service with the given options.
func NewService(opts ...Option) Service {
cfg := &config{}
......@@ -87,6 +96,9 @@ func NewService(opts ...Option) Service {
if cfg.enableTags {
exts = append(exts, extensions.TagExtension)
}
if cfg.enableMentions {
exts = append(exts, extensions.MentionExtension)
}
md := goldmark.New(
goldmark.WithExtensions(exts...),
......@@ -330,6 +342,7 @@ func (s *service) ExtractAll(content []byte) (*ExtractedData, error) {
data := &ExtractedData{
Tags: []string{},
Mentions: []string{},
Property: &storepb.MemoPayload_Property{},
}
......@@ -345,6 +358,9 @@ func (s *service) ExtractAll(content []byte) (*ExtractedData, error) {
if tagNode, ok := n.(*mast.TagNode); ok {
data.Tags = append(data.Tags, string(tagNode.Tag))
}
if mentionNode, ok := n.(*mast.MentionNode); ok {
data.Mentions = append(data.Mentions, strings.ToLower(string(mentionNode.Username)))
}
// Check if the first block-level child of the document is an H1 heading.
if !firstBlockChecked && n.Parent() != nil && n.Parent().Kind() == gast.KindDocument {
......@@ -382,6 +398,7 @@ func (s *service) ExtractAll(content []byte) (*ExtractedData, error) {
// Deduplicate tags while preserving original case
data.Tags = uniquePreserveCase(data.Tags)
data.Mentions = uniquePreserveCase(data.Mentions)
return data, nil
}
......
......@@ -340,6 +340,15 @@ func TestExtractAllTitle(t *testing.T) {
}
}
func TestExtractAllMentions(t *testing.T) {
svc := NewService(WithTagExtension(), WithMentionExtension())
data, err := svc.ExtractAll([]byte("Hi @Alice and @bob. Email support@example.com should stay plain. #tag"))
require.NoError(t, err)
assert.ElementsMatch(t, []string{"alice", "bob"}, data.Mentions)
assert.ElementsMatch(t, []string{"tag"}, data.Tags)
}
func TestExtractTags(t *testing.T) {
tests := []struct {
name string
......
package parser
import (
"unicode"
"unicode/utf8"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
mast "github.com/usememos/memos/internal/markdown/ast"
)
const (
// MaxMentionLength matches the username token length accepted by the API.
MaxMentionLength = 32
)
type mentionParser struct{}
// NewMentionParser creates a new inline parser for @mention syntax.
func NewMentionParser() parser.InlineParser {
return &mentionParser{}
}
// Trigger returns the characters that trigger this parser.
func (*mentionParser) Trigger() []byte {
return []byte{'@'}
}
func isValidMentionRune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '-'
}
func isMentionBoundary(r rune) bool {
return unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r)
}
// Parse parses @mention syntax while avoiding email-address matches.
func (*mentionParser) Parse(_ gast.Node, block text.Reader, _ parser.Context) gast.Node {
line, _ := block.PeekLine()
if len(line) == 0 || line[0] != '@' {
return nil
}
prev := block.PrecendingCharacter()
if prev != '\n' && !isMentionBoundary(prev) {
return nil
}
start := 1
pos := start
runeCount := 0
hasLetterOrNumber := false
for pos < len(line) {
r, size := utf8.DecodeRune(line[pos:])
if r == utf8.RuneError && size == 1 {
break
}
if !isValidMentionRune(r) {
break
}
if unicode.IsLetter(r) || unicode.IsNumber(r) {
hasLetterOrNumber = true
}
runeCount++
if runeCount > MaxMentionLength {
break
}
pos += size
}
if pos <= start || !hasLetterOrNumber {
return nil
}
username := line[start:pos]
usernameCopy := make([]byte, len(username))
copy(usernameCopy, username)
block.Advance(pos)
return &mast.MentionNode{
Username: usernameCopy,
}
}
......@@ -156,6 +156,10 @@ func (r *MarkdownRenderer) renderNode(node gast.Node, source []byte, depth int)
r.buf.WriteByte('#')
r.buf.Write(n.Tag)
case *mast.MentionNode:
r.buf.WriteByte('@')
r.buf.Write(n.Username)
default:
// For unknown nodes, try to render children
r.renderChildren(n, source, depth)
......
......@@ -19,6 +19,14 @@ service UserService {
option (google.api.http) = {get: "/api/v1/users"};
}
// BatchGetUsers returns active users by usernames.
rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse) {
option (google.api.http) = {
post: "/api/v1/users:batchGet"
body: "*"
};
}
// GetUser gets a user by username.
// Format: users/{username} (e.g., users/steven)
rpc GetUser(GetUserRequest) returns (User) {
......@@ -242,6 +250,14 @@ message ListUsersResponse {
int32 total_size = 3;
}
message BatchGetUsersRequest {
repeated string usernames = 1;
}
message BatchGetUsersResponse {
repeated User users = 1;
}
message GetUserRequest {
// Required. The resource name of the user.
// Format: users/{username}
......@@ -612,6 +628,9 @@ message UserNotification {
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
// The sender user details.
User sender_user = 8 [(google.api.field_behavior) = OUTPUT_ONLY];
// The status of the notification.
Status status = 3 [(google.api.field_behavior) = OPTIONAL];
......@@ -623,6 +642,7 @@ message UserNotification {
oneof payload {
MemoCommentPayload memo_comment = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
MemoMentionPayload memo_mention = 7 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message MemoCommentPayload {
......@@ -633,6 +653,28 @@ message UserNotification {
// The name of related memo.
// Format: memos/{memo}
string related_memo = 2;
// Preview text of the comment memo.
string memo_snippet = 3;
// Preview text of the related memo.
string related_memo_snippet = 4;
}
message MemoMentionPayload {
// The memo that contains the mention.
// Format: memos/{memo}
string memo = 1;
// The related parent memo when the mention was created in a comment.
// Format: memos/{memo}
string related_memo = 2;
// Preview text of the memo that contains the mention.
string memo_snippet = 3;
// Preview text of the related parent memo.
string related_memo_snippet = 4;
}
enum Status {
......@@ -644,6 +686,7 @@ message UserNotification {
enum Type {
TYPE_UNSPECIFIED = 0;
MEMO_COMMENT = 1;
MEMO_MENTION = 2;
}
}
......
......@@ -36,6 +36,9 @@ const (
const (
// UserServiceListUsersProcedure is the fully-qualified name of the UserService's ListUsers RPC.
UserServiceListUsersProcedure = "/memos.api.v1.UserService/ListUsers"
// UserServiceBatchGetUsersProcedure is the fully-qualified name of the UserService's BatchGetUsers
// RPC.
UserServiceBatchGetUsersProcedure = "/memos.api.v1.UserService/BatchGetUsers"
// UserServiceGetUserProcedure is the fully-qualified name of the UserService's GetUser RPC.
UserServiceGetUserProcedure = "/memos.api.v1.UserService/GetUser"
// UserServiceCreateUserProcedure is the fully-qualified name of the UserService's CreateUser RPC.
......@@ -95,6 +98,8 @@ const (
type UserServiceClient interface {
// ListUsers returns a list of users.
ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error)
// BatchGetUsers returns active users by usernames.
BatchGetUsers(context.Context, *connect.Request[v1.BatchGetUsersRequest]) (*connect.Response[v1.BatchGetUsersResponse], error)
// GetUser gets a user by username.
// Format: users/{username} (e.g., users/steven)
GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error)
......@@ -155,6 +160,12 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
connect.WithSchema(userServiceMethods.ByName("ListUsers")),
connect.WithClientOptions(opts...),
),
batchGetUsers: connect.NewClient[v1.BatchGetUsersRequest, v1.BatchGetUsersResponse](
httpClient,
baseURL+UserServiceBatchGetUsersProcedure,
connect.WithSchema(userServiceMethods.ByName("BatchGetUsers")),
connect.WithClientOptions(opts...),
),
getUser: connect.NewClient[v1.GetUserRequest, v1.User](
httpClient,
baseURL+UserServiceGetUserProcedure,
......@@ -275,6 +286,7 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
// userServiceClient implements UserServiceClient.
type userServiceClient struct {
listUsers *connect.Client[v1.ListUsersRequest, v1.ListUsersResponse]
batchGetUsers *connect.Client[v1.BatchGetUsersRequest, v1.BatchGetUsersResponse]
getUser *connect.Client[v1.GetUserRequest, v1.User]
createUser *connect.Client[v1.CreateUserRequest, v1.User]
updateUser *connect.Client[v1.UpdateUserRequest, v1.User]
......@@ -301,6 +313,11 @@ func (c *userServiceClient) ListUsers(ctx context.Context, req *connect.Request[
return c.listUsers.CallUnary(ctx, req)
}
// BatchGetUsers calls memos.api.v1.UserService.BatchGetUsers.
func (c *userServiceClient) BatchGetUsers(ctx context.Context, req *connect.Request[v1.BatchGetUsersRequest]) (*connect.Response[v1.BatchGetUsersResponse], error) {
return c.batchGetUsers.CallUnary(ctx, req)
}
// GetUser calls memos.api.v1.UserService.GetUser.
func (c *userServiceClient) GetUser(ctx context.Context, req *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) {
return c.getUser.CallUnary(ctx, req)
......@@ -400,6 +417,8 @@ func (c *userServiceClient) DeleteUserNotification(ctx context.Context, req *con
type UserServiceHandler interface {
// ListUsers returns a list of users.
ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error)
// BatchGetUsers returns active users by usernames.
BatchGetUsers(context.Context, *connect.Request[v1.BatchGetUsersRequest]) (*connect.Response[v1.BatchGetUsersResponse], error)
// GetUser gets a user by username.
// Format: users/{username} (e.g., users/steven)
GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error)
......@@ -456,6 +475,12 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
connect.WithSchema(userServiceMethods.ByName("ListUsers")),
connect.WithHandlerOptions(opts...),
)
userServiceBatchGetUsersHandler := connect.NewUnaryHandler(
UserServiceBatchGetUsersProcedure,
svc.BatchGetUsers,
connect.WithSchema(userServiceMethods.ByName("BatchGetUsers")),
connect.WithHandlerOptions(opts...),
)
userServiceGetUserHandler := connect.NewUnaryHandler(
UserServiceGetUserProcedure,
svc.GetUser,
......@@ -574,6 +599,8 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
switch r.URL.Path {
case UserServiceListUsersProcedure:
userServiceListUsersHandler.ServeHTTP(w, r)
case UserServiceBatchGetUsersProcedure:
userServiceBatchGetUsersHandler.ServeHTTP(w, r)
case UserServiceGetUserProcedure:
userServiceGetUserHandler.ServeHTTP(w, r)
case UserServiceCreateUserProcedure:
......@@ -625,6 +652,10 @@ func (UnimplementedUserServiceHandler) ListUsers(context.Context, *connect.Reque
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUsers is not implemented"))
}
func (UnimplementedUserServiceHandler) BatchGetUsers(context.Context, *connect.Request[v1.BatchGetUsersRequest]) (*connect.Response[v1.BatchGetUsersResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.BatchGetUsers is not implemented"))
}
func (UnimplementedUserServiceHandler) GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.GetUser is not implemented"))
}
......
......@@ -126,7 +126,7 @@ func (x UserSetting_Key) Number() protoreflect.EnumNumber {
// Deprecated: Use UserSetting_Key.Descriptor instead.
func (UserSetting_Key) EnumDescriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 0}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13, 0}
}
type UserNotification_Status int32
......@@ -175,7 +175,7 @@ func (x UserNotification_Status) Number() protoreflect.EnumNumber {
// Deprecated: Use UserNotification_Status.Descriptor instead.
func (UserNotification_Status) EnumDescriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 0}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 0}
}
type UserNotification_Type int32
......@@ -183,6 +183,7 @@ type UserNotification_Type int32
const (
UserNotification_TYPE_UNSPECIFIED UserNotification_Type = 0
UserNotification_MEMO_COMMENT UserNotification_Type = 1
UserNotification_MEMO_MENTION UserNotification_Type = 2
)
// Enum value maps for UserNotification_Type.
......@@ -190,10 +191,12 @@ var (
UserNotification_Type_name = map[int32]string{
0: "TYPE_UNSPECIFIED",
1: "MEMO_COMMENT",
2: "MEMO_MENTION",
}
UserNotification_Type_value = map[string]int32{
"TYPE_UNSPECIFIED": 0,
"MEMO_COMMENT": 1,
"MEMO_MENTION": 2,
}
)
......@@ -221,7 +224,7 @@ func (x UserNotification_Type) Number() protoreflect.EnumNumber {
// Deprecated: Use UserNotification_Type.Descriptor instead.
func (UserNotification_Type) EnumDescriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 1}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 1}
}
type User struct {
......@@ -503,6 +506,94 @@ func (x *ListUsersResponse) GetTotalSize() int32 {
return 0
}
type BatchGetUsersRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Usernames []string `protobuf:"bytes,1,rep,name=usernames,proto3" json:"usernames,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *BatchGetUsersRequest) Reset() {
*x = BatchGetUsersRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BatchGetUsersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchGetUsersRequest) ProtoMessage() {}
func (x *BatchGetUsersRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchGetUsersRequest.ProtoReflect.Descriptor instead.
func (*BatchGetUsersRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{3}
}
func (x *BatchGetUsersRequest) GetUsernames() []string {
if x != nil {
return x.Usernames
}
return nil
}
type BatchGetUsersResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *BatchGetUsersResponse) Reset() {
*x = BatchGetUsersResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BatchGetUsersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchGetUsersResponse) ProtoMessage() {}
func (x *BatchGetUsersResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchGetUsersResponse.ProtoReflect.Descriptor instead.
func (*BatchGetUsersResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{4}
}
func (x *BatchGetUsersResponse) GetUsers() []*User {
if x != nil {
return x.Users
}
return nil
}
type GetUserRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the user.
......@@ -517,7 +608,7 @@ type GetUserRequest struct {
func (x *GetUserRequest) Reset() {
*x = GetUserRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[3]
mi := &file_api_v1_user_service_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -529,7 +620,7 @@ func (x *GetUserRequest) String() string {
func (*GetUserRequest) ProtoMessage() {}
func (x *GetUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[3]
mi := &file_api_v1_user_service_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -542,7 +633,7 @@ func (x *GetUserRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead.
func (*GetUserRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{3}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{5}
}
func (x *GetUserRequest) GetName() string {
......@@ -578,7 +669,7 @@ type CreateUserRequest struct {
func (x *CreateUserRequest) Reset() {
*x = CreateUserRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[4]
mi := &file_api_v1_user_service_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -590,7 +681,7 @@ func (x *CreateUserRequest) String() string {
func (*CreateUserRequest) ProtoMessage() {}
func (x *CreateUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[4]
mi := &file_api_v1_user_service_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -603,7 +694,7 @@ func (x *CreateUserRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead.
func (*CreateUserRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{4}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{6}
}
func (x *CreateUserRequest) GetUser() *User {
......@@ -648,7 +739,7 @@ type UpdateUserRequest struct {
func (x *UpdateUserRequest) Reset() {
*x = UpdateUserRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[5]
mi := &file_api_v1_user_service_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -660,7 +751,7 @@ func (x *UpdateUserRequest) String() string {
func (*UpdateUserRequest) ProtoMessage() {}
func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[5]
mi := &file_api_v1_user_service_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -673,7 +764,7 @@ func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{5}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{7}
}
func (x *UpdateUserRequest) GetUser() *User {
......@@ -710,7 +801,7 @@ type DeleteUserRequest struct {
func (x *DeleteUserRequest) Reset() {
*x = DeleteUserRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[6]
mi := &file_api_v1_user_service_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -722,7 +813,7 @@ func (x *DeleteUserRequest) String() string {
func (*DeleteUserRequest) ProtoMessage() {}
func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[6]
mi := &file_api_v1_user_service_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -735,7 +826,7 @@ func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{6}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{8}
}
func (x *DeleteUserRequest) GetName() string {
......@@ -774,7 +865,7 @@ type UserStats struct {
func (x *UserStats) Reset() {
*x = UserStats{}
mi := &file_api_v1_user_service_proto_msgTypes[7]
mi := &file_api_v1_user_service_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -786,7 +877,7 @@ func (x *UserStats) String() string {
func (*UserStats) ProtoMessage() {}
func (x *UserStats) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[7]
mi := &file_api_v1_user_service_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -799,7 +890,7 @@ func (x *UserStats) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserStats.ProtoReflect.Descriptor instead.
func (*UserStats) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{7}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{9}
}
func (x *UserStats) GetName() string {
......@@ -855,7 +946,7 @@ type GetUserStatsRequest struct {
func (x *GetUserStatsRequest) Reset() {
*x = GetUserStatsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[8]
mi := &file_api_v1_user_service_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -867,7 +958,7 @@ func (x *GetUserStatsRequest) String() string {
func (*GetUserStatsRequest) ProtoMessage() {}
func (x *GetUserStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[8]
mi := &file_api_v1_user_service_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -880,7 +971,7 @@ func (x *GetUserStatsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetUserStatsRequest.ProtoReflect.Descriptor instead.
func (*GetUserStatsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{8}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{10}
}
func (x *GetUserStatsRequest) GetName() string {
......@@ -898,7 +989,7 @@ type ListAllUserStatsRequest struct {
func (x *ListAllUserStatsRequest) Reset() {
*x = ListAllUserStatsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[9]
mi := &file_api_v1_user_service_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -910,7 +1001,7 @@ func (x *ListAllUserStatsRequest) String() string {
func (*ListAllUserStatsRequest) ProtoMessage() {}
func (x *ListAllUserStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[9]
mi := &file_api_v1_user_service_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -923,7 +1014,7 @@ func (x *ListAllUserStatsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAllUserStatsRequest.ProtoReflect.Descriptor instead.
func (*ListAllUserStatsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{9}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{11}
}
type ListAllUserStatsResponse struct {
......@@ -936,7 +1027,7 @@ type ListAllUserStatsResponse struct {
func (x *ListAllUserStatsResponse) Reset() {
*x = ListAllUserStatsResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[10]
mi := &file_api_v1_user_service_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -948,7 +1039,7 @@ func (x *ListAllUserStatsResponse) String() string {
func (*ListAllUserStatsResponse) ProtoMessage() {}
func (x *ListAllUserStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[10]
mi := &file_api_v1_user_service_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -961,7 +1052,7 @@ func (x *ListAllUserStatsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAllUserStatsResponse.ProtoReflect.Descriptor instead.
func (*ListAllUserStatsResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{10}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{12}
}
func (x *ListAllUserStatsResponse) GetStats() []*UserStats {
......@@ -989,7 +1080,7 @@ type UserSetting struct {
func (x *UserSetting) Reset() {
*x = UserSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[11]
mi := &file_api_v1_user_service_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1001,7 +1092,7 @@ func (x *UserSetting) String() string {
func (*UserSetting) ProtoMessage() {}
func (x *UserSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[11]
mi := &file_api_v1_user_service_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1014,7 +1105,7 @@ func (x *UserSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserSetting.ProtoReflect.Descriptor instead.
func (*UserSetting) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{11}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13}
}
func (x *UserSetting) GetName() string {
......@@ -1076,7 +1167,7 @@ type GetUserSettingRequest struct {
func (x *GetUserSettingRequest) Reset() {
*x = GetUserSettingRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[12]
mi := &file_api_v1_user_service_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1088,7 +1179,7 @@ func (x *GetUserSettingRequest) String() string {
func (*GetUserSettingRequest) ProtoMessage() {}
func (x *GetUserSettingRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[12]
mi := &file_api_v1_user_service_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1101,7 +1192,7 @@ func (x *GetUserSettingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetUserSettingRequest.ProtoReflect.Descriptor instead.
func (*GetUserSettingRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{12}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{14}
}
func (x *GetUserSettingRequest) GetName() string {
......@@ -1123,7 +1214,7 @@ type UpdateUserSettingRequest struct {
func (x *UpdateUserSettingRequest) Reset() {
*x = UpdateUserSettingRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[13]
mi := &file_api_v1_user_service_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1135,7 +1226,7 @@ func (x *UpdateUserSettingRequest) String() string {
func (*UpdateUserSettingRequest) ProtoMessage() {}
func (x *UpdateUserSettingRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[13]
mi := &file_api_v1_user_service_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1148,7 +1239,7 @@ func (x *UpdateUserSettingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserSettingRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserSettingRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{15}
}
func (x *UpdateUserSettingRequest) GetSetting() *UserSetting {
......@@ -1185,7 +1276,7 @@ type ListUserSettingsRequest struct {
func (x *ListUserSettingsRequest) Reset() {
*x = ListUserSettingsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[14]
mi := &file_api_v1_user_service_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1197,7 +1288,7 @@ func (x *ListUserSettingsRequest) String() string {
func (*ListUserSettingsRequest) ProtoMessage() {}
func (x *ListUserSettingsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[14]
mi := &file_api_v1_user_service_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1210,7 +1301,7 @@ func (x *ListUserSettingsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserSettingsRequest.ProtoReflect.Descriptor instead.
func (*ListUserSettingsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{14}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{16}
}
func (x *ListUserSettingsRequest) GetParent() string {
......@@ -1250,7 +1341,7 @@ type ListUserSettingsResponse struct {
func (x *ListUserSettingsResponse) Reset() {
*x = ListUserSettingsResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[15]
mi := &file_api_v1_user_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1262,7 +1353,7 @@ func (x *ListUserSettingsResponse) String() string {
func (*ListUserSettingsResponse) ProtoMessage() {}
func (x *ListUserSettingsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[15]
mi := &file_api_v1_user_service_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1275,7 +1366,7 @@ func (x *ListUserSettingsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserSettingsResponse.ProtoReflect.Descriptor instead.
func (*ListUserSettingsResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{15}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{17}
}
func (x *ListUserSettingsResponse) GetSettings() []*UserSetting {
......@@ -1320,7 +1411,7 @@ type PersonalAccessToken struct {
func (x *PersonalAccessToken) Reset() {
*x = PersonalAccessToken{}
mi := &file_api_v1_user_service_proto_msgTypes[16]
mi := &file_api_v1_user_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1332,7 +1423,7 @@ func (x *PersonalAccessToken) String() string {
func (*PersonalAccessToken) ProtoMessage() {}
func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[16]
mi := &file_api_v1_user_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1345,7 +1436,7 @@ func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message {
// Deprecated: Use PersonalAccessToken.ProtoReflect.Descriptor instead.
func (*PersonalAccessToken) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{16}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{18}
}
func (x *PersonalAccessToken) GetName() string {
......@@ -1398,7 +1489,7 @@ type ListPersonalAccessTokensRequest struct {
func (x *ListPersonalAccessTokensRequest) Reset() {
*x = ListPersonalAccessTokensRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[17]
mi := &file_api_v1_user_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1410,7 +1501,7 @@ func (x *ListPersonalAccessTokensRequest) String() string {
func (*ListPersonalAccessTokensRequest) ProtoMessage() {}
func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[17]
mi := &file_api_v1_user_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1423,7 +1514,7 @@ func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListPersonalAccessTokensRequest.ProtoReflect.Descriptor instead.
func (*ListPersonalAccessTokensRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{17}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{19}
}
func (x *ListPersonalAccessTokensRequest) GetParent() string {
......@@ -1461,7 +1552,7 @@ type ListPersonalAccessTokensResponse struct {
func (x *ListPersonalAccessTokensResponse) Reset() {
*x = ListPersonalAccessTokensResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[18]
mi := &file_api_v1_user_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1473,7 +1564,7 @@ func (x *ListPersonalAccessTokensResponse) String() string {
func (*ListPersonalAccessTokensResponse) ProtoMessage() {}
func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[18]
mi := &file_api_v1_user_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1486,7 +1577,7 @@ func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListPersonalAccessTokensResponse.ProtoReflect.Descriptor instead.
func (*ListPersonalAccessTokensResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{18}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20}
}
func (x *ListPersonalAccessTokensResponse) GetPersonalAccessTokens() []*PersonalAccessToken {
......@@ -1525,7 +1616,7 @@ type CreatePersonalAccessTokenRequest struct {
func (x *CreatePersonalAccessTokenRequest) Reset() {
*x = CreatePersonalAccessTokenRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[19]
mi := &file_api_v1_user_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1537,7 +1628,7 @@ func (x *CreatePersonalAccessTokenRequest) String() string {
func (*CreatePersonalAccessTokenRequest) ProtoMessage() {}
func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[19]
mi := &file_api_v1_user_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1550,7 +1641,7 @@ func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreatePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.
func (*CreatePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{19}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21}
}
func (x *CreatePersonalAccessTokenRequest) GetParent() string {
......@@ -1587,7 +1678,7 @@ type CreatePersonalAccessTokenResponse struct {
func (x *CreatePersonalAccessTokenResponse) Reset() {
*x = CreatePersonalAccessTokenResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[20]
mi := &file_api_v1_user_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1599,7 +1690,7 @@ func (x *CreatePersonalAccessTokenResponse) String() string {
func (*CreatePersonalAccessTokenResponse) ProtoMessage() {}
func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[20]
mi := &file_api_v1_user_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1612,7 +1703,7 @@ func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message
// Deprecated: Use CreatePersonalAccessTokenResponse.ProtoReflect.Descriptor instead.
func (*CreatePersonalAccessTokenResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{22}
}
func (x *CreatePersonalAccessTokenResponse) GetPersonalAccessToken() *PersonalAccessToken {
......@@ -1640,7 +1731,7 @@ type DeletePersonalAccessTokenRequest struct {
func (x *DeletePersonalAccessTokenRequest) Reset() {
*x = DeletePersonalAccessTokenRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[21]
mi := &file_api_v1_user_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1652,7 +1743,7 @@ func (x *DeletePersonalAccessTokenRequest) String() string {
func (*DeletePersonalAccessTokenRequest) ProtoMessage() {}
func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[21]
mi := &file_api_v1_user_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1665,7 +1756,7 @@ func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeletePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.
func (*DeletePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{23}
}
func (x *DeletePersonalAccessTokenRequest) GetName() string {
......@@ -1695,7 +1786,7 @@ type UserWebhook struct {
func (x *UserWebhook) Reset() {
*x = UserWebhook{}
mi := &file_api_v1_user_service_proto_msgTypes[22]
mi := &file_api_v1_user_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1707,7 +1798,7 @@ func (x *UserWebhook) String() string {
func (*UserWebhook) ProtoMessage() {}
func (x *UserWebhook) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[22]
mi := &file_api_v1_user_service_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1720,7 +1811,7 @@ func (x *UserWebhook) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserWebhook.ProtoReflect.Descriptor instead.
func (*UserWebhook) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{22}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{24}
}
func (x *UserWebhook) GetName() string {
......@@ -1769,7 +1860,7 @@ type ListUserWebhooksRequest struct {
func (x *ListUserWebhooksRequest) Reset() {
*x = ListUserWebhooksRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[23]
mi := &file_api_v1_user_service_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1781,7 +1872,7 @@ func (x *ListUserWebhooksRequest) String() string {
func (*ListUserWebhooksRequest) ProtoMessage() {}
func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[23]
mi := &file_api_v1_user_service_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1794,7 +1885,7 @@ func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserWebhooksRequest.ProtoReflect.Descriptor instead.
func (*ListUserWebhooksRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{23}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{25}
}
func (x *ListUserWebhooksRequest) GetParent() string {
......@@ -1814,7 +1905,7 @@ type ListUserWebhooksResponse struct {
func (x *ListUserWebhooksResponse) Reset() {
*x = ListUserWebhooksResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[24]
mi := &file_api_v1_user_service_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1826,7 +1917,7 @@ func (x *ListUserWebhooksResponse) String() string {
func (*ListUserWebhooksResponse) ProtoMessage() {}
func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[24]
mi := &file_api_v1_user_service_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1839,7 +1930,7 @@ func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserWebhooksResponse.ProtoReflect.Descriptor instead.
func (*ListUserWebhooksResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{24}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{26}
}
func (x *ListUserWebhooksResponse) GetWebhooks() []*UserWebhook {
......@@ -1862,7 +1953,7 @@ type CreateUserWebhookRequest struct {
func (x *CreateUserWebhookRequest) Reset() {
*x = CreateUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[25]
mi := &file_api_v1_user_service_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1874,7 +1965,7 @@ func (x *CreateUserWebhookRequest) String() string {
func (*CreateUserWebhookRequest) ProtoMessage() {}
func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[25]
mi := &file_api_v1_user_service_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1887,7 +1978,7 @@ func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*CreateUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{25}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{27}
}
func (x *CreateUserWebhookRequest) GetParent() string {
......@@ -1916,7 +2007,7 @@ type UpdateUserWebhookRequest struct {
func (x *UpdateUserWebhookRequest) Reset() {
*x = UpdateUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[26]
mi := &file_api_v1_user_service_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1928,7 +2019,7 @@ func (x *UpdateUserWebhookRequest) String() string {
func (*UpdateUserWebhookRequest) ProtoMessage() {}
func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[26]
mi := &file_api_v1_user_service_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1941,7 +2032,7 @@ func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{26}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28}
}
func (x *UpdateUserWebhookRequest) GetWebhook() *UserWebhook {
......@@ -1969,7 +2060,7 @@ type DeleteUserWebhookRequest struct {
func (x *DeleteUserWebhookRequest) Reset() {
*x = DeleteUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[27]
mi := &file_api_v1_user_service_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1981,7 +2072,7 @@ func (x *DeleteUserWebhookRequest) String() string {
func (*DeleteUserWebhookRequest) ProtoMessage() {}
func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[27]
mi := &file_api_v1_user_service_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1994,7 +2085,7 @@ func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{27}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{29}
}
func (x *DeleteUserWebhookRequest) GetName() string {
......@@ -2012,6 +2103,8 @@ type UserNotification struct {
// The sender of the notification.
// Format: users/{user}
Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"`
// The sender user details.
SenderUser *User `protobuf:"bytes,8,opt,name=sender_user,json=senderUser,proto3" json:"sender_user,omitempty"`
// The status of the notification.
Status UserNotification_Status `protobuf:"varint,3,opt,name=status,proto3,enum=memos.api.v1.UserNotification_Status" json:"status,omitempty"`
// The creation timestamp.
......@@ -2021,6 +2114,7 @@ type UserNotification struct {
// Types that are valid to be assigned to Payload:
//
// *UserNotification_MemoComment
// *UserNotification_MemoMention
Payload isUserNotification_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
......@@ -2028,7 +2122,7 @@ type UserNotification struct {
func (x *UserNotification) Reset() {
*x = UserNotification{}
mi := &file_api_v1_user_service_proto_msgTypes[28]
mi := &file_api_v1_user_service_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2040,7 +2134,7 @@ func (x *UserNotification) String() string {
func (*UserNotification) ProtoMessage() {}
func (x *UserNotification) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[28]
mi := &file_api_v1_user_service_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2053,7 +2147,7 @@ func (x *UserNotification) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserNotification.ProtoReflect.Descriptor instead.
func (*UserNotification) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30}
}
func (x *UserNotification) GetName() string {
......@@ -2070,6 +2164,13 @@ func (x *UserNotification) GetSender() string {
return ""
}
func (x *UserNotification) GetSenderUser() *User {
if x != nil {
return x.SenderUser
}
return nil
}
func (x *UserNotification) GetStatus() UserNotification_Status {
if x != nil {
return x.Status
......@@ -2107,6 +2208,15 @@ func (x *UserNotification) GetMemoComment() *UserNotification_MemoCommentPayload
return nil
}
func (x *UserNotification) GetMemoMention() *UserNotification_MemoMentionPayload {
if x != nil {
if x, ok := x.Payload.(*UserNotification_MemoMention); ok {
return x.MemoMention
}
}
return nil
}
type isUserNotification_Payload interface {
isUserNotification_Payload()
}
......@@ -2115,8 +2225,14 @@ type UserNotification_MemoComment struct {
MemoComment *UserNotification_MemoCommentPayload `protobuf:"bytes,6,opt,name=memo_comment,json=memoComment,proto3,oneof"`
}
type UserNotification_MemoMention struct {
MemoMention *UserNotification_MemoMentionPayload `protobuf:"bytes,7,opt,name=memo_mention,json=memoMention,proto3,oneof"`
}
func (*UserNotification_MemoComment) isUserNotification_Payload() {}
func (*UserNotification_MemoMention) isUserNotification_Payload() {}
type ListUserNotificationsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The parent user resource.
......@@ -2131,7 +2247,7 @@ type ListUserNotificationsRequest struct {
func (x *ListUserNotificationsRequest) Reset() {
*x = ListUserNotificationsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[29]
mi := &file_api_v1_user_service_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2143,7 +2259,7 @@ func (x *ListUserNotificationsRequest) String() string {
func (*ListUserNotificationsRequest) ProtoMessage() {}
func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[29]
mi := &file_api_v1_user_service_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2156,7 +2272,7 @@ func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserNotificationsRequest.ProtoReflect.Descriptor instead.
func (*ListUserNotificationsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{29}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{31}
}
func (x *ListUserNotificationsRequest) GetParent() string {
......@@ -2197,7 +2313,7 @@ type ListUserNotificationsResponse struct {
func (x *ListUserNotificationsResponse) Reset() {
*x = ListUserNotificationsResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[30]
mi := &file_api_v1_user_service_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2209,7 +2325,7 @@ func (x *ListUserNotificationsResponse) String() string {
func (*ListUserNotificationsResponse) ProtoMessage() {}
func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[30]
mi := &file_api_v1_user_service_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2222,7 +2338,7 @@ func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserNotificationsResponse.ProtoReflect.Descriptor instead.
func (*ListUserNotificationsResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{32}
}
func (x *ListUserNotificationsResponse) GetNotifications() []*UserNotification {
......@@ -2249,7 +2365,7 @@ type UpdateUserNotificationRequest struct {
func (x *UpdateUserNotificationRequest) Reset() {
*x = UpdateUserNotificationRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[31]
mi := &file_api_v1_user_service_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2261,7 +2377,7 @@ func (x *UpdateUserNotificationRequest) String() string {
func (*UpdateUserNotificationRequest) ProtoMessage() {}
func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[31]
mi := &file_api_v1_user_service_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2274,7 +2390,7 @@ func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserNotificationRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserNotificationRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{31}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{33}
}
func (x *UpdateUserNotificationRequest) GetNotification() *UserNotification {
......@@ -2301,7 +2417,7 @@ type DeleteUserNotificationRequest struct {
func (x *DeleteUserNotificationRequest) Reset() {
*x = DeleteUserNotificationRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[32]
mi := &file_api_v1_user_service_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2313,7 +2429,7 @@ func (x *DeleteUserNotificationRequest) String() string {
func (*DeleteUserNotificationRequest) ProtoMessage() {}
func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[32]
mi := &file_api_v1_user_service_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2326,7 +2442,7 @@ func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteUserNotificationRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserNotificationRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{32}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{34}
}
func (x *DeleteUserNotificationRequest) GetName() string {
......@@ -2349,7 +2465,7 @@ type UserStats_MemoTypeStats struct {
func (x *UserStats_MemoTypeStats) Reset() {
*x = UserStats_MemoTypeStats{}
mi := &file_api_v1_user_service_proto_msgTypes[34]
mi := &file_api_v1_user_service_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2361,7 +2477,7 @@ func (x *UserStats_MemoTypeStats) String() string {
func (*UserStats_MemoTypeStats) ProtoMessage() {}
func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[34]
mi := &file_api_v1_user_service_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2374,7 +2490,7 @@ func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserStats_MemoTypeStats.ProtoReflect.Descriptor instead.
func (*UserStats_MemoTypeStats) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{7, 1}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{9, 1}
}
func (x *UserStats_MemoTypeStats) GetLinkCount() int32 {
......@@ -2422,7 +2538,7 @@ type UserSetting_GeneralSetting struct {
func (x *UserSetting_GeneralSetting) Reset() {
*x = UserSetting_GeneralSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[35]
mi := &file_api_v1_user_service_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2434,7 +2550,7 @@ func (x *UserSetting_GeneralSetting) String() string {
func (*UserSetting_GeneralSetting) ProtoMessage() {}
func (x *UserSetting_GeneralSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[35]
mi := &file_api_v1_user_service_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2447,7 +2563,7 @@ func (x *UserSetting_GeneralSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserSetting_GeneralSetting.ProtoReflect.Descriptor instead.
func (*UserSetting_GeneralSetting) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 0}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13, 0}
}
func (x *UserSetting_GeneralSetting) GetLocale() string {
......@@ -2482,7 +2598,7 @@ type UserSetting_WebhooksSetting struct {
func (x *UserSetting_WebhooksSetting) Reset() {
*x = UserSetting_WebhooksSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[36]
mi := &file_api_v1_user_service_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2494,7 +2610,7 @@ func (x *UserSetting_WebhooksSetting) String() string {
func (*UserSetting_WebhooksSetting) ProtoMessage() {}
func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[36]
mi := &file_api_v1_user_service_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2507,7 +2623,7 @@ func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserSetting_WebhooksSetting.ProtoReflect.Descriptor instead.
func (*UserSetting_WebhooksSetting) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 1}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13, 1}
}
func (x *UserSetting_WebhooksSetting) GetWebhooks() []*UserWebhook {
......@@ -2525,13 +2641,17 @@ type UserNotification_MemoCommentPayload struct {
// The name of related memo.
// Format: memos/{memo}
RelatedMemo string `protobuf:"bytes,2,opt,name=related_memo,json=relatedMemo,proto3" json:"related_memo,omitempty"`
// Preview text of the comment memo.
MemoSnippet string `protobuf:"bytes,3,opt,name=memo_snippet,json=memoSnippet,proto3" json:"memo_snippet,omitempty"`
// Preview text of the related memo.
RelatedMemoSnippet string `protobuf:"bytes,4,opt,name=related_memo_snippet,json=relatedMemoSnippet,proto3" json:"related_memo_snippet,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserNotification_MemoCommentPayload) Reset() {
*x = UserNotification_MemoCommentPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[37]
mi := &file_api_v1_user_service_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2543,7 +2663,7 @@ func (x *UserNotification_MemoCommentPayload) String() string {
func (*UserNotification_MemoCommentPayload) ProtoMessage() {}
func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[37]
mi := &file_api_v1_user_service_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2556,7 +2676,7 @@ func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Messag
// Deprecated: Use UserNotification_MemoCommentPayload.ProtoReflect.Descriptor instead.
func (*UserNotification_MemoCommentPayload) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 0}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 0}
}
func (x *UserNotification_MemoCommentPayload) GetMemo() string {
......@@ -2573,6 +2693,94 @@ func (x *UserNotification_MemoCommentPayload) GetRelatedMemo() string {
return ""
}
func (x *UserNotification_MemoCommentPayload) GetMemoSnippet() string {
if x != nil {
return x.MemoSnippet
}
return ""
}
func (x *UserNotification_MemoCommentPayload) GetRelatedMemoSnippet() string {
if x != nil {
return x.RelatedMemoSnippet
}
return ""
}
type UserNotification_MemoMentionPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The memo that contains the mention.
// Format: memos/{memo}
Memo string `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"`
// The related parent memo when the mention was created in a comment.
// Format: memos/{memo}
RelatedMemo string `protobuf:"bytes,2,opt,name=related_memo,json=relatedMemo,proto3" json:"related_memo,omitempty"`
// Preview text of the memo that contains the mention.
MemoSnippet string `protobuf:"bytes,3,opt,name=memo_snippet,json=memoSnippet,proto3" json:"memo_snippet,omitempty"`
// Preview text of the related parent memo.
RelatedMemoSnippet string `protobuf:"bytes,4,opt,name=related_memo_snippet,json=relatedMemoSnippet,proto3" json:"related_memo_snippet,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserNotification_MemoMentionPayload) Reset() {
*x = UserNotification_MemoMentionPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserNotification_MemoMentionPayload) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserNotification_MemoMentionPayload) ProtoMessage() {}
func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserNotification_MemoMentionPayload.ProtoReflect.Descriptor instead.
func (*UserNotification_MemoMentionPayload) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 1}
}
func (x *UserNotification_MemoMentionPayload) GetMemo() string {
if x != nil {
return x.Memo
}
return ""
}
func (x *UserNotification_MemoMentionPayload) GetRelatedMemo() string {
if x != nil {
return x.RelatedMemo
}
return ""
}
func (x *UserNotification_MemoMentionPayload) GetMemoSnippet() string {
if x != nil {
return x.MemoSnippet
}
return ""
}
func (x *UserNotification_MemoMentionPayload) GetRelatedMemoSnippet() string {
if x != nil {
return x.RelatedMemoSnippet
}
return ""
}
var File_api_v1_user_service_proto protoreflect.FileDescriptor
const file_api_v1_user_service_proto_rawDesc = "" +
......@@ -2609,7 +2817,11 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\x05users\x18\x01 \x03(\v2\x12.memos.api.v1.UserR\x05users\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" +
"\n" +
"total_size\x18\x03 \x01(\x05R\ttotalSize\"}\n" +
"total_size\x18\x03 \x01(\x05R\ttotalSize\"4\n" +
"\x14BatchGetUsersRequest\x12\x1c\n" +
"\tusernames\x18\x01 \x03(\tR\tusernames\"A\n" +
"\x15BatchGetUsersResponse\x12(\n" +
"\x05users\x18\x01 \x03(\v2\x12.memos.api.v1.UserR\x05users\"}\n" +
"\x0eGetUserRequest\x12-\n" +
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x04name\x12<\n" +
......@@ -2741,27 +2953,38 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskR\n" +
"updateMask\"3\n" +
"\x18DeleteUserWebhookRequest\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\x02R\x04name\"\xb8\x05\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\x02R\x04name\"\xda\b\n" +
"\x10UserNotification\x12\x1a\n" +
"\x04name\x18\x01 \x01(\tB\x06\xe0A\x03\xe0A\bR\x04name\x121\n" +
"\x06sender\x18\x02 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x06sender\x12B\n" +
"\x11memos.api.v1/UserR\x06sender\x128\n" +
"\vsender_user\x18\b \x01(\v2\x12.memos.api.v1.UserB\x03\xe0A\x03R\n" +
"senderUser\x12B\n" +
"\x06status\x18\x03 \x01(\x0e2%.memos.api.v1.UserNotification.StatusB\x03\xe0A\x01R\x06status\x12@\n" +
"\vcreate_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" +
"createTime\x12<\n" +
"\x04type\x18\x05 \x01(\x0e2#.memos.api.v1.UserNotification.TypeB\x03\xe0A\x03R\x04type\x12[\n" +
"\fmemo_comment\x18\x06 \x01(\v21.memos.api.v1.UserNotification.MemoCommentPayloadB\x03\xe0A\x03H\x00R\vmemoComment\x1aK\n" +
"\fmemo_comment\x18\x06 \x01(\v21.memos.api.v1.UserNotification.MemoCommentPayloadB\x03\xe0A\x03H\x00R\vmemoComment\x12[\n" +
"\fmemo_mention\x18\a \x01(\v21.memos.api.v1.UserNotification.MemoMentionPayloadB\x03\xe0A\x03H\x00R\vmemoMention\x1a\xa0\x01\n" +
"\x12MemoCommentPayload\x12\x12\n" +
"\x04memo\x18\x01 \x01(\tR\x04memo\x12!\n" +
"\frelated_memo\x18\x02 \x01(\tR\vrelatedMemo\":\n" +
"\frelated_memo\x18\x02 \x01(\tR\vrelatedMemo\x12!\n" +
"\fmemo_snippet\x18\x03 \x01(\tR\vmemoSnippet\x120\n" +
"\x14related_memo_snippet\x18\x04 \x01(\tR\x12relatedMemoSnippet\x1a\xa0\x01\n" +
"\x12MemoMentionPayload\x12\x12\n" +
"\x04memo\x18\x01 \x01(\tR\x04memo\x12!\n" +
"\frelated_memo\x18\x02 \x01(\tR\vrelatedMemo\x12!\n" +
"\fmemo_snippet\x18\x03 \x01(\tR\vmemoSnippet\x120\n" +
"\x14related_memo_snippet\x18\x04 \x01(\tR\x12relatedMemoSnippet\":\n" +
"\x06Status\x12\x16\n" +
"\x12STATUS_UNSPECIFIED\x10\x00\x12\n" +
"\n" +
"\x06UNREAD\x10\x01\x12\f\n" +
"\bARCHIVED\x10\x02\".\n" +
"\bARCHIVED\x10\x02\"@\n" +
"\x04Type\x12\x14\n" +
"\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" +
"\fMEMO_COMMENT\x10\x01:p\xeaAm\n" +
"\fMEMO_COMMENT\x10\x01\x12\x10\n" +
"\fMEMO_MENTION\x10\x02:p\xeaAm\n" +
"\x1dmemos.api.v1/UserNotification\x12)users/{user}/notifications/{notification}\x1a\x04name*\rnotifications2\fnotificationB\t\n" +
"\apayload\"\xb4\x01\n" +
"\x1cListUserNotificationsRequest\x121\n" +
......@@ -2780,9 +3003,10 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"updateMask\"Z\n" +
"\x1dDeleteUserNotificationRequest\x129\n" +
"\x04name\x18\x01 \x01(\tB%\xe0A\x02\xfaA\x1f\n" +
"\x1dmemos.api.v1/UserNotificationR\x04name2\x83\x17\n" +
"\x1dmemos.api.v1/UserNotificationR\x04name2\x80\x18\n" +
"\vUserService\x12c\n" +
"\tListUsers\x12\x1e.memos.api.v1.ListUsersRequest\x1a\x1f.memos.api.v1.ListUsersResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/users\x12b\n" +
"\tListUsers\x12\x1e.memos.api.v1.ListUsersRequest\x1a\x1f.memos.api.v1.ListUsersResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/users\x12{\n" +
"\rBatchGetUsers\x12\".memos.api.v1.BatchGetUsersRequest\x1a#.memos.api.v1.BatchGetUsersResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/v1/users:batchGet\x12b\n" +
"\aGetUser\x12\x1c.memos.api.v1.GetUserRequest\x1a\x12.memos.api.v1.User\"%\xdaA\x04name\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/{name=users/*}\x12e\n" +
"\n" +
"CreateUser\x12\x1f.memos.api.v1.CreateUserRequest\x1a\x12.memos.api.v1.User\"\"\xdaA\x04user\x82\xd3\xe4\x93\x02\x15:\x04user\"\r/api/v1/users\x12\x7f\n" +
......@@ -2820,7 +3044,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte {
}
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 38)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 41)
var file_api_v1_user_service_proto_goTypes = []any{
(User_Role)(0), // 0: memos.api.v1.User.Role
(UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key
......@@ -2829,129 +3053,137 @@ var file_api_v1_user_service_proto_goTypes = []any{
(*User)(nil), // 4: memos.api.v1.User
(*ListUsersRequest)(nil), // 5: memos.api.v1.ListUsersRequest
(*ListUsersResponse)(nil), // 6: memos.api.v1.ListUsersResponse
(*GetUserRequest)(nil), // 7: memos.api.v1.GetUserRequest
(*CreateUserRequest)(nil), // 8: memos.api.v1.CreateUserRequest
(*UpdateUserRequest)(nil), // 9: memos.api.v1.UpdateUserRequest
(*DeleteUserRequest)(nil), // 10: memos.api.v1.DeleteUserRequest
(*UserStats)(nil), // 11: memos.api.v1.UserStats
(*GetUserStatsRequest)(nil), // 12: memos.api.v1.GetUserStatsRequest
(*ListAllUserStatsRequest)(nil), // 13: memos.api.v1.ListAllUserStatsRequest
(*ListAllUserStatsResponse)(nil), // 14: memos.api.v1.ListAllUserStatsResponse
(*UserSetting)(nil), // 15: memos.api.v1.UserSetting
(*GetUserSettingRequest)(nil), // 16: memos.api.v1.GetUserSettingRequest
(*UpdateUserSettingRequest)(nil), // 17: memos.api.v1.UpdateUserSettingRequest
(*ListUserSettingsRequest)(nil), // 18: memos.api.v1.ListUserSettingsRequest
(*ListUserSettingsResponse)(nil), // 19: memos.api.v1.ListUserSettingsResponse
(*PersonalAccessToken)(nil), // 20: memos.api.v1.PersonalAccessToken
(*ListPersonalAccessTokensRequest)(nil), // 21: memos.api.v1.ListPersonalAccessTokensRequest
(*ListPersonalAccessTokensResponse)(nil), // 22: memos.api.v1.ListPersonalAccessTokensResponse
(*CreatePersonalAccessTokenRequest)(nil), // 23: memos.api.v1.CreatePersonalAccessTokenRequest
(*CreatePersonalAccessTokenResponse)(nil), // 24: memos.api.v1.CreatePersonalAccessTokenResponse
(*DeletePersonalAccessTokenRequest)(nil), // 25: memos.api.v1.DeletePersonalAccessTokenRequest
(*UserWebhook)(nil), // 26: memos.api.v1.UserWebhook
(*ListUserWebhooksRequest)(nil), // 27: memos.api.v1.ListUserWebhooksRequest
(*ListUserWebhooksResponse)(nil), // 28: memos.api.v1.ListUserWebhooksResponse
(*CreateUserWebhookRequest)(nil), // 29: memos.api.v1.CreateUserWebhookRequest
(*UpdateUserWebhookRequest)(nil), // 30: memos.api.v1.UpdateUserWebhookRequest
(*DeleteUserWebhookRequest)(nil), // 31: memos.api.v1.DeleteUserWebhookRequest
(*UserNotification)(nil), // 32: memos.api.v1.UserNotification
(*ListUserNotificationsRequest)(nil), // 33: memos.api.v1.ListUserNotificationsRequest
(*ListUserNotificationsResponse)(nil), // 34: memos.api.v1.ListUserNotificationsResponse
(*UpdateUserNotificationRequest)(nil), // 35: memos.api.v1.UpdateUserNotificationRequest
(*DeleteUserNotificationRequest)(nil), // 36: memos.api.v1.DeleteUserNotificationRequest
nil, // 37: memos.api.v1.UserStats.TagCountEntry
(*UserStats_MemoTypeStats)(nil), // 38: memos.api.v1.UserStats.MemoTypeStats
(*UserSetting_GeneralSetting)(nil), // 39: memos.api.v1.UserSetting.GeneralSetting
(*UserSetting_WebhooksSetting)(nil), // 40: memos.api.v1.UserSetting.WebhooksSetting
(*UserNotification_MemoCommentPayload)(nil), // 41: memos.api.v1.UserNotification.MemoCommentPayload
(State)(0), // 42: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 44: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 45: google.protobuf.Empty
(*BatchGetUsersRequest)(nil), // 7: memos.api.v1.BatchGetUsersRequest
(*BatchGetUsersResponse)(nil), // 8: memos.api.v1.BatchGetUsersResponse
(*GetUserRequest)(nil), // 9: memos.api.v1.GetUserRequest
(*CreateUserRequest)(nil), // 10: memos.api.v1.CreateUserRequest
(*UpdateUserRequest)(nil), // 11: memos.api.v1.UpdateUserRequest
(*DeleteUserRequest)(nil), // 12: memos.api.v1.DeleteUserRequest
(*UserStats)(nil), // 13: memos.api.v1.UserStats
(*GetUserStatsRequest)(nil), // 14: memos.api.v1.GetUserStatsRequest
(*ListAllUserStatsRequest)(nil), // 15: memos.api.v1.ListAllUserStatsRequest
(*ListAllUserStatsResponse)(nil), // 16: memos.api.v1.ListAllUserStatsResponse
(*UserSetting)(nil), // 17: memos.api.v1.UserSetting
(*GetUserSettingRequest)(nil), // 18: memos.api.v1.GetUserSettingRequest
(*UpdateUserSettingRequest)(nil), // 19: memos.api.v1.UpdateUserSettingRequest
(*ListUserSettingsRequest)(nil), // 20: memos.api.v1.ListUserSettingsRequest
(*ListUserSettingsResponse)(nil), // 21: memos.api.v1.ListUserSettingsResponse
(*PersonalAccessToken)(nil), // 22: memos.api.v1.PersonalAccessToken
(*ListPersonalAccessTokensRequest)(nil), // 23: memos.api.v1.ListPersonalAccessTokensRequest
(*ListPersonalAccessTokensResponse)(nil), // 24: memos.api.v1.ListPersonalAccessTokensResponse
(*CreatePersonalAccessTokenRequest)(nil), // 25: memos.api.v1.CreatePersonalAccessTokenRequest
(*CreatePersonalAccessTokenResponse)(nil), // 26: memos.api.v1.CreatePersonalAccessTokenResponse
(*DeletePersonalAccessTokenRequest)(nil), // 27: memos.api.v1.DeletePersonalAccessTokenRequest
(*UserWebhook)(nil), // 28: memos.api.v1.UserWebhook
(*ListUserWebhooksRequest)(nil), // 29: memos.api.v1.ListUserWebhooksRequest
(*ListUserWebhooksResponse)(nil), // 30: memos.api.v1.ListUserWebhooksResponse
(*CreateUserWebhookRequest)(nil), // 31: memos.api.v1.CreateUserWebhookRequest
(*UpdateUserWebhookRequest)(nil), // 32: memos.api.v1.UpdateUserWebhookRequest
(*DeleteUserWebhookRequest)(nil), // 33: memos.api.v1.DeleteUserWebhookRequest
(*UserNotification)(nil), // 34: memos.api.v1.UserNotification
(*ListUserNotificationsRequest)(nil), // 35: memos.api.v1.ListUserNotificationsRequest
(*ListUserNotificationsResponse)(nil), // 36: memos.api.v1.ListUserNotificationsResponse
(*UpdateUserNotificationRequest)(nil), // 37: memos.api.v1.UpdateUserNotificationRequest
(*DeleteUserNotificationRequest)(nil), // 38: memos.api.v1.DeleteUserNotificationRequest
nil, // 39: memos.api.v1.UserStats.TagCountEntry
(*UserStats_MemoTypeStats)(nil), // 40: memos.api.v1.UserStats.MemoTypeStats
(*UserSetting_GeneralSetting)(nil), // 41: memos.api.v1.UserSetting.GeneralSetting
(*UserSetting_WebhooksSetting)(nil), // 42: memos.api.v1.UserSetting.WebhooksSetting
(*UserNotification_MemoCommentPayload)(nil), // 43: memos.api.v1.UserNotification.MemoCommentPayload
(*UserNotification_MemoMentionPayload)(nil), // 44: memos.api.v1.UserNotification.MemoMentionPayload
(State)(0), // 45: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 47: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 48: google.protobuf.Empty
}
var file_api_v1_user_service_proto_depIdxs = []int32{
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
42, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
43, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
43, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
45, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
46, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
46, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
4, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
44, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
4, // 6: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
4, // 7: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
44, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
43, // 9: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
38, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
37, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
11, // 12: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
39, // 13: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
40, // 14: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
15, // 15: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
44, // 16: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
15, // 17: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
43, // 18: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
43, // 19: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
43, // 20: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
20, // 21: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
20, // 22: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
43, // 23: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
43, // 24: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
26, // 25: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
26, // 26: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
26, // 27: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
44, // 28: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
2, // 29: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
43, // 30: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
3, // 31: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
41, // 32: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
32, // 33: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
32, // 34: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
44, // 35: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
26, // 36: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
5, // 37: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
7, // 38: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
8, // 39: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
9, // 40: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
10, // 41: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
13, // 42: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
12, // 43: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
16, // 44: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
17, // 45: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
18, // 46: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
21, // 47: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
23, // 48: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
25, // 49: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
27, // 50: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
29, // 51: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
30, // 52: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
31, // 53: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
33, // 54: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
35, // 55: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
36, // 56: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
6, // 57: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
4, // 58: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
4, // 59: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
4, // 60: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
45, // 61: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
14, // 62: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
11, // 63: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
15, // 64: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
15, // 65: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
19, // 66: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
22, // 67: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
24, // 68: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
45, // 69: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
28, // 70: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
26, // 71: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
26, // 72: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
45, // 73: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
34, // 74: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
32, // 75: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
45, // 76: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
57, // [57:77] is the sub-list for method output_type
37, // [37:57] is the sub-list for method input_type
37, // [37:37] is the sub-list for extension type_name
37, // [37:37] is the sub-list for extension extendee
0, // [0:37] is the sub-list for field type_name
4, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User
47, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
4, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
4, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
47, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
46, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
40, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
39, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
13, // 13: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
41, // 14: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
42, // 15: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
17, // 16: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
47, // 17: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
17, // 18: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
46, // 19: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
46, // 20: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
46, // 21: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
22, // 22: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
22, // 23: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
46, // 24: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
46, // 25: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
28, // 26: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
28, // 27: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
28, // 28: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
47, // 29: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
4, // 30: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
2, // 31: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
46, // 32: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
3, // 33: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
43, // 34: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
44, // 35: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
34, // 36: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
34, // 37: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
47, // 38: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
28, // 39: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
5, // 40: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
7, // 41: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
9, // 42: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
10, // 43: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
11, // 44: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
12, // 45: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
15, // 46: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
14, // 47: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
18, // 48: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
19, // 49: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
20, // 50: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
23, // 51: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
25, // 52: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
27, // 53: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
29, // 54: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
31, // 55: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
32, // 56: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
33, // 57: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
35, // 58: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
37, // 59: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
38, // 60: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
6, // 61: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
8, // 62: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
4, // 63: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
4, // 64: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
4, // 65: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
48, // 66: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
16, // 67: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
13, // 68: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
17, // 69: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
17, // 70: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
21, // 71: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
24, // 72: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
26, // 73: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
48, // 74: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
30, // 75: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
28, // 76: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
28, // 77: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
48, // 78: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
36, // 79: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
34, // 80: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
48, // 81: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
61, // [61:82] is the sub-list for method output_type
40, // [40:61] is the sub-list for method input_type
40, // [40:40] is the sub-list for extension type_name
40, // [40:40] is the sub-list for extension extendee
0, // [0:40] is the sub-list for field type_name
}
func init() { file_api_v1_user_service_proto_init() }
......@@ -2960,12 +3192,13 @@ func file_api_v1_user_service_proto_init() {
return
}
file_api_v1_common_proto_init()
file_api_v1_user_service_proto_msgTypes[11].OneofWrappers = []any{
file_api_v1_user_service_proto_msgTypes[13].OneofWrappers = []any{
(*UserSetting_GeneralSetting_)(nil),
(*UserSetting_WebhooksSetting_)(nil),
}
file_api_v1_user_service_proto_msgTypes[28].OneofWrappers = []any{
file_api_v1_user_service_proto_msgTypes[30].OneofWrappers = []any{
(*UserNotification_MemoComment)(nil),
(*UserNotification_MemoMention)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
......@@ -2973,7 +3206,7 @@ func file_api_v1_user_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),
NumEnums: 4,
NumMessages: 38,
NumMessages: 41,
NumExtensions: 0,
NumServices: 1,
},
......
......@@ -70,6 +70,33 @@ func local_request_UserService_ListUsers_0(ctx context.Context, marshaler runtim
return msg, metadata, err
}
func request_UserService_BatchGetUsers_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq BatchGetUsersRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.BatchGetUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_BatchGetUsers_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq BatchGetUsersRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.BatchGetUsers(ctx, &protoReq)
return msg, metadata, err
}
var filter_UserService_GetUser_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
func request_UserService_GetUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
......@@ -1071,6 +1098,26 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_BatchGetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/BatchGetUsers", runtime.WithHTTPPathPattern("/api/v1/users:batchGet"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_BatchGetUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_BatchGetUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
......@@ -1508,6 +1555,23 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_BatchGetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/BatchGetUsers", runtime.WithHTTPPathPattern("/api/v1/users:batchGet"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_BatchGetUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_BatchGetUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
......@@ -1836,6 +1900,7 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
var (
pattern_UserService_ListUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_BatchGetUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "batchGet"))
pattern_UserService_GetUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, ""))
......@@ -1859,6 +1924,7 @@ var (
var (
forward_UserService_ListUsers_0 = runtime.ForwardResponseMessage
forward_UserService_BatchGetUsers_0 = runtime.ForwardResponseMessage
forward_UserService_GetUser_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage
......
......@@ -21,6 +21,7 @@ const _ = grpc.SupportPackageIsVersion9
const (
UserService_ListUsers_FullMethodName = "/memos.api.v1.UserService/ListUsers"
UserService_BatchGetUsers_FullMethodName = "/memos.api.v1.UserService/BatchGetUsers"
UserService_GetUser_FullMethodName = "/memos.api.v1.UserService/GetUser"
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
......@@ -48,6 +49,8 @@ const (
type UserServiceClient interface {
// ListUsers returns a list of users.
ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error)
// BatchGetUsers returns active users by usernames.
BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersResponse, error)
// GetUser gets a user by username.
// Format: users/{username} (e.g., users/steven)
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
......@@ -109,6 +112,16 @@ func (c *userServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest,
return out, nil
}
func (c *userServiceClient) BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchGetUsersResponse)
err := c.cc.Invoke(ctx, UserService_BatchGetUsers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(User)
......@@ -305,6 +318,8 @@ func (c *userServiceClient) DeleteUserNotification(ctx context.Context, in *Dele
type UserServiceServer interface {
// ListUsers returns a list of users.
ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error)
// BatchGetUsers returns active users by usernames.
BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error)
// GetUser gets a user by username.
// Format: users/{username} (e.g., users/steven)
GetUser(context.Context, *GetUserRequest) (*User, error)
......@@ -359,6 +374,9 @@ type UnimplementedUserServiceServer struct{}
func (UnimplementedUserServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUsers not implemented")
}
func (UnimplementedUserServiceServer) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchGetUsers not implemented")
}
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*User, error) {
return nil, status.Error(codes.Unimplemented, "method GetUser not implemented")
}
......@@ -455,6 +473,24 @@ func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec fu
return interceptor(ctx, in, info, handler)
}
func _UserService_BatchGetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchGetUsersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).BatchGetUsers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_BatchGetUsers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).BatchGetUsers(ctx, req.(*BatchGetUsersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserRequest)
if err := dec(in); err != nil {
......@@ -808,6 +844,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListUsers",
Handler: _UserService_ListUsers_Handler,
},
{
MethodName: "BatchGetUsers",
Handler: _UserService_BatchGetUsers_Handler,
},
{
MethodName: "GetUser",
Handler: _UserService_GetUser_Handler,
......
......@@ -1977,6 +1977,31 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users:batchGet:
post:
tags:
- UserService
description: BatchGetUsers returns active users by usernames.
operationId: UserService_BatchGetUsers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/BatchGetUsersRequest'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/BatchGetUsersResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users:stats:
get:
tags:
......@@ -2050,6 +2075,20 @@ components:
type: array
items:
type: string
BatchGetUsersRequest:
type: object
properties:
usernames:
type: array
items:
type: string
BatchGetUsersResponse:
type: object
properties:
users:
type: array
items:
$ref: '#/components/schemas/User'
Color:
type: object
properties:
......@@ -3192,6 +3231,11 @@ components:
description: |-
The sender of the notification.
Format: users/{user}
senderUser:
readOnly: true
allOf:
- $ref: '#/components/schemas/User'
description: The sender user details.
status:
enum:
- STATUS_UNSPECIFIED
......@@ -3210,6 +3254,7 @@ components:
enum:
- TYPE_UNSPECIFIED
- MEMO_COMMENT
- MEMO_MENTION
type: string
description: The type of the notification.
format: enum
......@@ -3217,6 +3262,10 @@ components:
readOnly: true
allOf:
- $ref: '#/components/schemas/UserNotification_MemoCommentPayload'
memoMention:
readOnly: true
allOf:
- $ref: '#/components/schemas/UserNotification_MemoMentionPayload'
UserNotification_MemoCommentPayload:
type: object
properties:
......@@ -3230,6 +3279,31 @@ components:
description: |-
The name of related memo.
Format: memos/{memo}
memoSnippet:
type: string
description: Preview text of the comment memo.
relatedMemoSnippet:
type: string
description: Preview text of the related memo.
UserNotification_MemoMentionPayload:
type: object
properties:
memo:
type: string
description: |-
The memo that contains the mention.
Format: memos/{memo}
relatedMemo:
type: string
description: |-
The related parent memo when the mention was created in a comment.
Format: memos/{memo}
memoSnippet:
type: string
description: Preview text of the memo that contains the mention.
relatedMemoSnippet:
type: string
description: Preview text of the related parent memo.
UserSetting:
type: object
properties:
......
......@@ -27,6 +27,8 @@ const (
InboxMessage_TYPE_UNSPECIFIED InboxMessage_Type = 0
// Memo comment notification.
InboxMessage_MEMO_COMMENT InboxMessage_Type = 1
// Memo mention notification.
InboxMessage_MEMO_MENTION InboxMessage_Type = 2
)
// Enum value maps for InboxMessage_Type.
......@@ -34,10 +36,12 @@ var (
InboxMessage_Type_name = map[int32]string{
0: "TYPE_UNSPECIFIED",
1: "MEMO_COMMENT",
2: "MEMO_MENTION",
}
InboxMessage_Type_value = map[string]int32{
"TYPE_UNSPECIFIED": 0,
"MEMO_COMMENT": 1,
"MEMO_MENTION": 2,
}
)
......@@ -75,6 +79,7 @@ type InboxMessage struct {
// Types that are valid to be assigned to Payload:
//
// *InboxMessage_MemoComment
// *InboxMessage_MemoMention
Payload isInboxMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
......@@ -133,6 +138,15 @@ func (x *InboxMessage) GetMemoComment() *InboxMessage_MemoCommentPayload {
return nil
}
func (x *InboxMessage) GetMemoMention() *InboxMessage_MemoMentionPayload {
if x != nil {
if x, ok := x.Payload.(*InboxMessage_MemoMention); ok {
return x.MemoMention
}
}
return nil
}
type isInboxMessage_Payload interface {
isInboxMessage_Payload()
}
......@@ -141,8 +155,14 @@ type InboxMessage_MemoComment struct {
MemoComment *InboxMessage_MemoCommentPayload `protobuf:"bytes,2,opt,name=memo_comment,json=memoComment,proto3,oneof"`
}
type InboxMessage_MemoMention struct {
MemoMention *InboxMessage_MemoMentionPayload `protobuf:"bytes,3,opt,name=memo_mention,json=memoMention,proto3,oneof"`
}
func (*InboxMessage_MemoComment) isInboxMessage_Payload() {}
func (*InboxMessage_MemoMention) isInboxMessage_Payload() {}
type InboxMessage_MemoCommentPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
MemoId int32 `protobuf:"varint,1,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"`
......@@ -195,20 +215,77 @@ func (x *InboxMessage_MemoCommentPayload) GetRelatedMemoId() int32 {
return 0
}
type InboxMessage_MemoMentionPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
MemoId int32 `protobuf:"varint,1,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"`
RelatedMemoId int32 `protobuf:"varint,2,opt,name=related_memo_id,json=relatedMemoId,proto3" json:"related_memo_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InboxMessage_MemoMentionPayload) Reset() {
*x = InboxMessage_MemoMentionPayload{}
mi := &file_store_inbox_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InboxMessage_MemoMentionPayload) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InboxMessage_MemoMentionPayload) ProtoMessage() {}
func (x *InboxMessage_MemoMentionPayload) ProtoReflect() protoreflect.Message {
mi := &file_store_inbox_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InboxMessage_MemoMentionPayload.ProtoReflect.Descriptor instead.
func (*InboxMessage_MemoMentionPayload) Descriptor() ([]byte, []int) {
return file_store_inbox_proto_rawDescGZIP(), []int{0, 1}
}
func (x *InboxMessage_MemoMentionPayload) GetMemoId() int32 {
if x != nil {
return x.MemoId
}
return 0
}
func (x *InboxMessage_MemoMentionPayload) GetRelatedMemoId() int32 {
if x != nil {
return x.RelatedMemoId
}
return 0
}
var File_store_inbox_proto protoreflect.FileDescriptor
const file_store_inbox_proto_rawDesc = "" +
"\n" +
"\x11store/inbox.proto\x12\vmemos.store\"\xa7\x02\n" +
"\x11store/inbox.proto\x12\vmemos.store\"\xe3\x03\n" +
"\fInboxMessage\x122\n" +
"\x04type\x18\x01 \x01(\x0e2\x1e.memos.store.InboxMessage.TypeR\x04type\x12Q\n" +
"\fmemo_comment\x18\x02 \x01(\v2,.memos.store.InboxMessage.MemoCommentPayloadH\x00R\vmemoComment\x1aU\n" +
"\fmemo_comment\x18\x02 \x01(\v2,.memos.store.InboxMessage.MemoCommentPayloadH\x00R\vmemoComment\x12Q\n" +
"\fmemo_mention\x18\x03 \x01(\v2,.memos.store.InboxMessage.MemoMentionPayloadH\x00R\vmemoMention\x1aU\n" +
"\x12MemoCommentPayload\x12\x17\n" +
"\amemo_id\x18\x01 \x01(\x05R\x06memoId\x12&\n" +
"\x0frelated_memo_id\x18\x02 \x01(\x05R\rrelatedMemoId\".\n" +
"\x0frelated_memo_id\x18\x02 \x01(\x05R\rrelatedMemoId\x1aU\n" +
"\x12MemoMentionPayload\x12\x17\n" +
"\amemo_id\x18\x01 \x01(\x05R\x06memoId\x12&\n" +
"\x0frelated_memo_id\x18\x02 \x01(\x05R\rrelatedMemoId\"@\n" +
"\x04Type\x12\x14\n" +
"\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" +
"\fMEMO_COMMENT\x10\x01B\t\n" +
"\fMEMO_COMMENT\x10\x01\x12\x10\n" +
"\fMEMO_MENTION\x10\x02B\t\n" +
"\apayloadB\x95\x01\n" +
"\x0fcom.memos.storeB\n" +
"InboxProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3"
......@@ -226,20 +303,22 @@ func file_store_inbox_proto_rawDescGZIP() []byte {
}
var file_store_inbox_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_store_inbox_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_store_inbox_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_store_inbox_proto_goTypes = []any{
(InboxMessage_Type)(0), // 0: memos.store.InboxMessage.Type
(*InboxMessage)(nil), // 1: memos.store.InboxMessage
(*InboxMessage_MemoCommentPayload)(nil), // 2: memos.store.InboxMessage.MemoCommentPayload
(*InboxMessage_MemoMentionPayload)(nil), // 3: memos.store.InboxMessage.MemoMentionPayload
}
var file_store_inbox_proto_depIdxs = []int32{
0, // 0: memos.store.InboxMessage.type:type_name -> memos.store.InboxMessage.Type
2, // 1: memos.store.InboxMessage.memo_comment:type_name -> memos.store.InboxMessage.MemoCommentPayload
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
3, // 2: memos.store.InboxMessage.memo_mention:type_name -> memos.store.InboxMessage.MemoMentionPayload
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_store_inbox_proto_init() }
......@@ -249,6 +328,7 @@ func file_store_inbox_proto_init() {
}
file_store_inbox_proto_msgTypes[0].OneofWrappers = []any{
(*InboxMessage_MemoComment)(nil),
(*InboxMessage_MemoMention)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
......@@ -256,7 +336,7 @@ func file_store_inbox_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_inbox_proto_rawDesc), len(file_store_inbox_proto_rawDesc)),
NumEnums: 1,
NumMessages: 2,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
......
......@@ -10,15 +10,23 @@ message InboxMessage {
int32 related_memo_id = 2;
}
message MemoMentionPayload {
int32 memo_id = 1;
int32 related_memo_id = 2;
}
// The type of the inbox message.
Type type = 1;
oneof payload {
MemoCommentPayload memo_comment = 2;
MemoMentionPayload memo_mention = 3;
}
enum Type {
TYPE_UNSPECIFIED = 0;
// Memo comment notification.
MEMO_COMMENT = 1;
// Memo mention notification.
MEMO_MENTION = 2;
}
}
......@@ -20,10 +20,10 @@ var PublicMethods = map[string]struct{}{
// User Service - public user profiles and stats
"/memos.api.v1.UserService/CreateUser": {}, // Allow first user registration
"/memos.api.v1.UserService/GetUser": {},
"/memos.api.v1.UserService/BatchGetUsers": {},
"/memos.api.v1.UserService/GetUserAvatar": {},
"/memos.api.v1.UserService/GetUserStats": {},
"/memos.api.v1.UserService/ListAllUserStats": {},
"/memos.api.v1.UserService/SearchUsers": {},
// Identity Provider Service - SSO buttons on login page
"/memos.api.v1.IdentityProviderService/ListIdentityProviders": {},
......
......@@ -18,10 +18,10 @@ func TestPublicMethodsArePublic(t *testing.T) {
// User Service
"/memos.api.v1.UserService/CreateUser",
"/memos.api.v1.UserService/GetUser",
"/memos.api.v1.UserService/BatchGetUsers",
"/memos.api.v1.UserService/GetUserAvatar",
"/memos.api.v1.UserService/GetUserStats",
"/memos.api.v1.UserService/ListAllUserStats",
"/memos.api.v1.UserService/SearchUsers",
// Identity Provider Service
"/memos.api.v1.IdentityProviderService/ListIdentityProviders",
// Memo Service
......
......@@ -79,6 +79,14 @@ func (s *ConnectServiceHandler) ListUsers(ctx context.Context, req *connect.Requ
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) BatchGetUsers(ctx context.Context, req *connect.Request[v1pb.BatchGetUsersRequest]) (*connect.Response[v1pb.BatchGetUsersResponse], error) {
resp, err := s.APIV1Service.BatchGetUsers(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) GetUser(ctx context.Context, req *connect.Request[v1pb.GetUserRequest]) (*connect.Response[v1pb.User], error) {
resp, err := s.APIV1Service.GetUser(ctx, req.Msg)
if err != nil {
......
package v1
import (
"context"
"github.com/usememos/memos/store"
)
func (s *APIV1Service) listMemosByID(ctx context.Context, memoIDs []int32) (map[int32]*store.Memo, error) {
if len(memoIDs) == 0 {
return map[int32]*store.Memo{}, nil
}
uniqueMemoIDs := make([]int32, 0, len(memoIDs))
seenMemoIDs := make(map[int32]struct{}, len(memoIDs))
for _, memoID := range memoIDs {
if _, seen := seenMemoIDs[memoID]; seen {
continue
}
seenMemoIDs[memoID] = struct{}{}
uniqueMemoIDs = append(uniqueMemoIDs, memoID)
}
memos, err := s.Store.ListMemos(ctx, &store.FindMemo{IDList: uniqueMemoIDs})
if err != nil {
return nil, err
}
memosByID := make(map[int32]*store.Memo, len(memos))
for _, memo := range memos {
memosByID[memo.ID] = memo
}
return memosByID, nil
}
package v1
import (
"context"
"log/slog"
"github.com/pkg/errors"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
// suppressMentionKey is a context key used to suppress mention notification side effects
// when CreateMemo is called internally from CreateMemoComment.
type suppressMentionKey struct{}
func withSuppressMentionNotifications(ctx context.Context) context.Context {
return context.WithValue(ctx, suppressMentionKey{}, true)
}
func isMentionNotificationSuppressed(ctx context.Context) bool {
v, ok := ctx.Value(suppressMentionKey{}).(bool)
return ok && v
}
func (s *APIV1Service) resolveMentionTargets(ctx context.Context, content string) (map[int32]*store.User, error) {
targets := make(map[int32]*store.User)
if content == "" {
return targets, nil
}
data, err := s.MarkdownService.ExtractAll([]byte(content))
if err != nil {
return nil, errors.Wrap(err, "failed to extract mentions")
}
if len(data.Mentions) == 0 {
return targets, nil
}
normal := store.Normal
users, err := s.Store.ListUsers(ctx, &store.FindUser{
UsernameList: data.Mentions,
RowStatus: &normal,
})
if err != nil {
return nil, errors.Wrap(err, "failed to resolve mention users")
}
for _, user := range users {
targets[user.ID] = user
}
return targets, nil
}
func canUserAccessMentionContext(target *store.User, memo *store.Memo, relatedMemo *store.Memo) bool {
if target == nil || memo == nil {
return false
}
if relatedMemo != nil {
if relatedMemo.Visibility == store.Private && target.ID != relatedMemo.CreatorID {
return false
}
}
if memo.Visibility == store.Private && target.ID != memo.CreatorID {
return false
}
return true
}
func shouldSkipMentionInbox(target *store.User, memo *store.Memo, relatedMemo *store.Memo) bool {
if target == nil || memo == nil {
return true
}
if target.ID == memo.CreatorID {
return true
}
// Comment creation already generates a memo-comment inbox item for the parent creator.
if relatedMemo != nil && target.ID == relatedMemo.CreatorID && memo.Visibility != store.Private && memo.CreatorID != relatedMemo.CreatorID {
return true
}
return !canUserAccessMentionContext(target, memo, relatedMemo)
}
func (s *APIV1Service) dispatchMemoMentionNotifications(ctx context.Context, memo *store.Memo, relatedMemo *store.Memo, previousContent string) error {
if memo == nil {
return nil
}
currentTargets, err := s.resolveMentionTargets(ctx, memo.Content)
if err != nil {
return err
}
if len(currentTargets) == 0 {
return nil
}
previousTargets, err := s.resolveMentionTargets(ctx, previousContent)
if err != nil {
return err
}
for userID, target := range currentTargets {
if _, exists := previousTargets[userID]; exists {
continue
}
if shouldSkipMentionInbox(target, memo, relatedMemo) {
continue
}
payload := &storepb.InboxMessage_MemoMentionPayload{
MemoId: memo.ID,
}
if relatedMemo != nil {
payload.RelatedMemoId = relatedMemo.ID
}
if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
SenderID: memo.CreatorID,
ReceiverID: target.ID,
Status: store.UNREAD,
Message: &storepb.InboxMessage{
Type: storepb.InboxMessage_MEMO_MENTION,
Payload: &storepb.InboxMessage_MemoMention{
MemoMention: payload,
},
},
}); err != nil {
return errors.Wrap(err, "failed to create mention inbox")
}
}
return nil
}
func (s *APIV1Service) dispatchMemoMentionNotificationsBestEffort(ctx context.Context, memo *store.Memo, relatedMemo *store.Memo, previousContent string) {
if err := s.dispatchMemoMentionNotifications(ctx, memo, relatedMemo, previousContent); err != nil {
slog.Warn("Failed to dispatch memo mention notifications", slog.Any("err", err), slog.Int64("memo_id", int64(memo.ID)))
}
}
......@@ -89,7 +89,7 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR
if len(create.Content) > contentLengthLimit {
return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit)
}
if err := memopayload.RebuildMemoPayload(create, s.MarkdownService); err != nil {
if err := memopayload.RebuildMemoPayload(ctx, create, s.MarkdownService); err != nil {
return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err)
}
if request.Memo.Location != nil {
......@@ -160,6 +160,10 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR
})
}
if !isMentionNotificationSuppressed(ctx) {
s.dispatchMemoMentionNotificationsBestEffort(ctx, memo, nil, "")
}
return memoMessage, nil
}
......@@ -433,8 +437,12 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
update := &store.UpdateMemo{
ID: memo.ID,
}
var previousContent string
contentUpdated := false
for _, path := range request.UpdateMask.Paths {
if path == "content" {
contentUpdated = true
previousContent = memo.Content
contentLengthLimit, err := s.getContentLengthLimit(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get content length limit")
......@@ -443,7 +451,7 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit)
}
memo.Content = request.Memo.Content
if err := memopayload.RebuildMemoPayload(memo, s.MarkdownService); err != nil {
if err := memopayload.RebuildMemoPayload(ctx, memo, s.MarkdownService); err != nil {
return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err)
}
update.Content = &memo.Content
......@@ -505,6 +513,9 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
if err != nil {
return nil, errors.Wrap(err, "failed to build updated memo state")
}
if contentUpdated {
s.dispatchMemoMentionNotificationsBestEffort(ctx, memo, parentMemo, previousContent)
}
s.dispatchMemoUpdatedSideEffects(ctx, memo, parentMemo, memoMessage)
return memoMessage, nil
......@@ -614,7 +625,7 @@ func (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.Crea
// Create the memo comment first; suppress the generic memo.created SSE event
// since CreateMemoComment broadcasts memo.comment.created for the parent instead.
memoComment, err := s.CreateMemo(withSuppressSSE(ctx), &v1pb.CreateMemoRequest{
memoComment, err := s.CreateMemo(withSuppressMentionNotifications(withSuppressSSE(ctx)), &v1pb.CreateMemoRequest{
Memo: request.Comment,
MemoId: request.CommentId,
})
......@@ -670,6 +681,8 @@ func (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.Crea
slog.Warn("Failed to dispatch memo comment created webhook", slog.Any("err", err))
}
s.dispatchMemoMentionNotificationsBestEffort(ctx, memo, relatedMemo, "")
// Broadcast live refresh event for the parent memo so subscribers see the new comment.
s.SSEHub.Broadcast(&SSEEvent{
Type: SSEEventMemoCommentCreated,
......
......@@ -42,6 +42,7 @@ func NewTestService(t *testing.T) *TestService {
secret := "test-secret"
markdownService := markdown.NewService(
markdown.WithTagExtension(),
markdown.WithMentionExtension(),
)
service := &apiv1.APIV1Service{
Secret: secret,
......
......@@ -6,6 +6,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/fieldmaskpb"
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
......@@ -51,10 +52,14 @@ func TestListUserNotificationsIncludesMemoCommentPayload(t *testing.T) {
notification := resp.Notifications[0]
require.Contains(t, notification.Name, fmt.Sprintf("users/%s/notifications/", owner.Username))
require.Equal(t, fmt.Sprintf("users/%s", commenter.Username), notification.Sender)
require.NotNil(t, notification.SenderUser)
require.Equal(t, commenter.Username, notification.SenderUser.Username)
require.Equal(t, apiv1.UserNotification_MEMO_COMMENT, notification.Type)
require.NotNil(t, notification.GetMemoComment())
require.Equal(t, comment.Name, notification.GetMemoComment().Memo)
require.Equal(t, memo.Name, notification.GetMemoComment().RelatedMemo)
require.Equal(t, "Comment content", notification.GetMemoComment().MemoSnippet)
require.Equal(t, "Base memo", notification.GetMemoComment().RelatedMemoSnippet)
}
func TestListUserNotificationsStoresMemoCommentPayloadInInbox(t *testing.T) {
......@@ -199,3 +204,142 @@ func TestListUserNotificationsRejectsNumericParent(t *testing.T) {
require.Error(t, err)
require.Contains(t, err.Error(), "invalid user name")
}
func TestListUserNotificationsIncludesMemoMentionPayload(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
author, err := ts.CreateRegularUser(ctx, "mention-author")
require.NoError(t, err)
authorCtx := ts.CreateUserContext(ctx, author.ID)
target, err := ts.CreateRegularUser(ctx, "mention-target")
require.NoError(t, err)
targetCtx := ts.CreateUserContext(ctx, target.ID)
memo, err := ts.Service.CreateMemo(authorCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: fmt.Sprintf("Hello @%s", target.Username),
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
resp, err := ts.Service.ListUserNotifications(targetCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%s", target.Username),
})
require.NoError(t, err)
require.Len(t, resp.Notifications, 1)
require.Equal(t, apiv1.UserNotification_MEMO_MENTION, resp.Notifications[0].Type)
require.NotNil(t, resp.Notifications[0].GetMemoMention())
require.Equal(t, memo.Name, resp.Notifications[0].GetMemoMention().Memo)
require.Empty(t, resp.Notifications[0].GetMemoMention().RelatedMemo)
require.Equal(t, author.Username, resp.Notifications[0].SenderUser.Username)
require.Equal(t, "Hello", resp.Notifications[0].GetMemoMention().MemoSnippet)
}
func TestCreateMemoCommentMentionDoesNotDuplicateOwnerNotification(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
owner, err := ts.CreateRegularUser(ctx, "mention-owner")
require.NoError(t, err)
ownerCtx := ts.CreateUserContext(ctx, owner.ID)
commenter, err := ts.CreateRegularUser(ctx, "mention-commenter")
require.NoError(t, err)
commenterCtx := ts.CreateUserContext(ctx, commenter.ID)
memo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Base memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
_, err = ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{
Name: memo.Name,
Comment: &apiv1.Memo{
Content: fmt.Sprintf("Hi @%s", owner.Username),
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
resp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%s", owner.Username),
})
require.NoError(t, err)
require.Len(t, resp.Notifications, 1)
require.Equal(t, apiv1.UserNotification_MEMO_COMMENT, resp.Notifications[0].Type)
}
func TestUpdateMemoMentionOnlyNotifiesNewTargets(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
author, err := ts.CreateRegularUser(ctx, "mention-update-author")
require.NoError(t, err)
authorCtx := ts.CreateUserContext(ctx, author.ID)
firstTarget, err := ts.CreateRegularUser(ctx, "mention-update-first")
require.NoError(t, err)
firstTargetCtx := ts.CreateUserContext(ctx, firstTarget.ID)
secondTarget, err := ts.CreateRegularUser(ctx, "mention-update-second")
require.NoError(t, err)
secondTargetCtx := ts.CreateUserContext(ctx, secondTarget.ID)
memo, err := ts.Service.CreateMemo(authorCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
updatedMemo, err := ts.Service.UpdateMemo(authorCtx, &apiv1.UpdateMemoRequest{
Memo: &apiv1.Memo{
Name: memo.Name,
Content: fmt.Sprintf("Hello @%s", firstTarget.Username),
Visibility: apiv1.Visibility_PUBLIC,
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"content"}},
})
require.NoError(t, err)
firstResp, err := ts.Service.ListUserNotifications(firstTargetCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%s", firstTarget.Username),
})
require.NoError(t, err)
require.Len(t, firstResp.Notifications, 1)
require.Equal(t, apiv1.UserNotification_MEMO_MENTION, firstResp.Notifications[0].Type)
require.Equal(t, updatedMemo.Name, firstResp.Notifications[0].GetMemoMention().Memo)
_, err = ts.Service.UpdateMemo(authorCtx, &apiv1.UpdateMemoRequest{
Memo: &apiv1.Memo{
Name: memo.Name,
Content: fmt.Sprintf("Hello again @%s and @%s", firstTarget.Username, secondTarget.Username),
Visibility: apiv1.Visibility_PUBLIC,
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"content"}},
})
require.NoError(t, err)
firstResp, err = ts.Service.ListUserNotifications(firstTargetCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%s", firstTarget.Username),
})
require.NoError(t, err)
require.Len(t, firstResp.Notifications, 1)
secondResp, err := ts.Service.ListUserNotifications(secondTargetCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%s", secondTarget.Username),
})
require.NoError(t, err)
require.Len(t, secondResp.Notifications, 1)
require.Equal(t, apiv1.UserNotification_MEMO_MENTION, secondResp.Notifications[0].Type)
}
package test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
)
func TestBatchGetUsersReturnsExactUsernamesWithoutAuthentication(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
_, err := ts.CreateRegularUser(ctx, "batch-alpha")
require.NoError(t, err)
_, err = ts.CreateRegularUser(ctx, "batch-beta")
require.NoError(t, err)
resp, err := ts.Service.BatchGetUsers(ctx, &apiv1.BatchGetUsersRequest{
Usernames: []string{"batch-alpha", "batch-beta", "missing-user", "batch-alpha"},
})
require.NoError(t, err)
require.Len(t, resp.Users, 2)
got := map[string]struct{}{}
for _, user := range resp.Users {
got[user.Username] = struct{}{}
}
_, ok := got["batch-alpha"]
require.True(t, ok)
_, ok = got["batch-beta"]
require.True(t, ok)
}
func TestBatchGetUsersRejectsTooManyUsernames(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
usernames := make([]string, 0, 101)
for i := range 101 {
usernames = append(usernames, fmt.Sprintf("user-%d", i))
}
_, err := ts.Service.BatchGetUsers(ctx, &apiv1.BatchGetUsersRequest{
Usernames: usernames,
})
require.Error(t, err)
require.Contains(t, err.Error(), "too many usernames")
}
......@@ -29,6 +29,8 @@ import (
"github.com/usememos/memos/store"
)
const maxBatchGetUsers = 100
func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
......@@ -70,6 +72,56 @@ func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersReq
return response, nil
}
func normalizeBatchUsernames(usernames []string) []string {
uniqueUsernames := make([]string, 0, len(usernames))
seen := make(map[string]struct{}, len(usernames))
for _, username := range usernames {
username = strings.TrimSpace(strings.ToLower(username))
if username == "" || !base.UIDMatcher.MatchString(username) {
continue
}
if _, ok := seen[username]; ok {
continue
}
seen[username] = struct{}{}
uniqueUsernames = append(uniqueUsernames, username)
}
return uniqueUsernames
}
func (s *APIV1Service) BatchGetUsers(ctx context.Context, request *v1pb.BatchGetUsersRequest) (*v1pb.BatchGetUsersResponse, error) {
if len(request.Usernames) == 0 {
return &v1pb.BatchGetUsersResponse{Users: []*v1pb.User{}}, nil
}
uniqueUsernames := normalizeBatchUsernames(request.Usernames)
if len(uniqueUsernames) > maxBatchGetUsers {
return nil, status.Errorf(codes.InvalidArgument, "too many usernames (max %d)", maxBatchGetUsers)
}
if len(uniqueUsernames) == 0 {
return &v1pb.BatchGetUsersResponse{Users: []*v1pb.User{}}, nil
}
normal := store.Normal
users, err := s.Store.ListUsers(ctx, &store.FindUser{
UsernameList: uniqueUsernames,
RowStatus: &normal,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
}
currentUser, _ := s.fetchCurrentUser(ctx)
response := &v1pb.BatchGetUsersResponse{
Users: make([]*v1pb.User, 0, len(users)),
}
for _, user := range users {
response.Users = append(response.Users, convertUserFromStore(user, currentUser))
}
return response, nil
}
func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest) (*v1pb.User, error) {
user, err := ResolveUserByName(ctx, s.Store, request.Name)
if err != nil {
......@@ -1269,12 +1321,9 @@ func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
// Fetch inbox items from storage
// Filter at database level to only include MEMO_COMMENT notifications (ignore legacy VERSION_UPDATE entries)
memoCommentType := storepb.InboxMessage_MEMO_COMMENT
// Fetch inbox items from storage.
inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
ReceiverID: &userID,
MessageType: &memoCommentType,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err)
......@@ -1289,10 +1338,14 @@ func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list notification users: %v", err)
}
memosByID, err := s.listMemosByID(ctx, collectInboxMemoIDs(inboxes))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list notification memos: %v", err)
}
notifications := []*v1pb.UserNotification{}
for _, inbox := range inboxes {
notification, err := s.convertInboxToUserNotificationWithUsers(ctx, inbox, usersByID)
notification, err := s.convertInboxToUserNotificationWithUsersAndMemos(inbox, currentUser, usersByID, memosByID)
if err != nil {
if status.Code(err) == codes.NotFound {
slog.Warn("Skipping notification with missing user",
......@@ -1304,6 +1357,9 @@ func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.
}
return nil, status.Errorf(codes.Internal, "failed to convert inbox: %v", err)
}
if notification.Type == v1pb.UserNotification_TYPE_UNSPECIFIED {
continue
}
notifications = append(notifications, notification)
}
......@@ -1379,7 +1435,7 @@ func (s *APIV1Service) UpdateUserNotification(ctx context.Context, request *v1pb
return nil, status.Errorf(codes.Internal, "failed to update inbox: %v", err)
}
notification, err := s.convertInboxToUserNotification(ctx, updatedInbox)
notification, err := s.convertInboxToUserNotification(ctx, updatedInbox, currentUser)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert inbox: %v", err)
}
......@@ -1432,15 +1488,43 @@ func (s *APIV1Service) DeleteUserNotification(ctx context.Context, request *v1pb
// convertInboxToUserNotification converts a storage-layer inbox to an API notification.
// This handles the mapping between the internal inbox representation and the public API.
func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox *store.Inbox) (*v1pb.UserNotification, error) {
func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox *store.Inbox, viewer *store.User) (*v1pb.UserNotification, error) {
usersByID, err := s.listUsersByID(ctx, []int32{inbox.ReceiverID, inbox.SenderID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list notification users: %v", err)
}
return s.convertInboxToUserNotificationWithUsers(ctx, inbox, usersByID)
memosByID, err := s.listMemosByID(ctx, collectInboxMemoIDs([]*store.Inbox{inbox}))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list notification memos: %v", err)
}
return s.convertInboxToUserNotificationWithUsersAndMemos(inbox, viewer, usersByID, memosByID)
}
func (s *APIV1Service) convertInboxToUserNotificationWithUsers(ctx context.Context, inbox *store.Inbox, usersByID map[int32]*store.User) (*v1pb.UserNotification, error) {
func collectInboxMemoIDs(inboxes []*store.Inbox) []int32 {
memoIDs := make([]int32, 0, len(inboxes)*2)
for _, inbox := range inboxes {
if inbox == nil || inbox.Message == nil {
continue
}
switch inbox.Message.Type {
case storepb.InboxMessage_MEMO_COMMENT:
payload := inbox.Message.GetMemoComment()
if payload != nil {
memoIDs = append(memoIDs, payload.MemoId, payload.RelatedMemoId)
}
case storepb.InboxMessage_MEMO_MENTION:
payload := inbox.Message.GetMemoMention()
if payload != nil {
memoIDs = append(memoIDs, payload.MemoId, payload.RelatedMemoId)
}
default:
// Ignore notification types without memo references.
}
}
return memoIDs
}
func (s *APIV1Service) convertInboxToUserNotificationWithUsersAndMemos(inbox *store.Inbox, viewer *store.User, usersByID map[int32]*store.User, memosByID map[int32]*store.Memo) (*v1pb.UserNotification, error) {
receiver := usersByID[inbox.ReceiverID]
if receiver == nil {
return nil, status.Errorf(codes.NotFound, "notification receiver not found")
......@@ -1453,6 +1537,7 @@ func (s *APIV1Service) convertInboxToUserNotificationWithUsers(ctx context.Conte
notification := &v1pb.UserNotification{
Name: fmt.Sprintf("%s/notifications/%d", BuildUserName(receiver.Username), inbox.ID),
Sender: BuildUserName(sender.Username),
SenderUser: convertUserFromStore(sender, viewer),
CreateTime: timestamppb.New(time.Unix(inbox.CreatedTs, 0)),
}
......@@ -1471,11 +1556,7 @@ func (s *APIV1Service) convertInboxToUserNotificationWithUsers(ctx context.Conte
switch inbox.Message.Type {
case storepb.InboxMessage_MEMO_COMMENT:
notification.Type = v1pb.UserNotification_MEMO_COMMENT
default:
notification.Type = v1pb.UserNotification_TYPE_UNSPECIFIED
}
payload, err := s.convertUserNotificationPayload(ctx, inbox.Message)
payload, err := s.convertMemoCommentNotificationPayload(viewer, inbox.Message, memosByID)
if err != nil {
return nil, err
}
......@@ -1484,41 +1565,117 @@ func (s *APIV1Service) convertInboxToUserNotificationWithUsers(ctx context.Conte
MemoComment: payload,
}
}
case storepb.InboxMessage_MEMO_MENTION:
notification.Type = v1pb.UserNotification_MEMO_MENTION
payload, err := s.convertMemoMentionNotificationPayload(viewer, inbox.Message, memosByID)
if err != nil {
return nil, err
}
if payload != nil {
notification.Payload = &v1pb.UserNotification_MemoMention{
MemoMention: payload,
}
}
default:
notification.Type = v1pb.UserNotification_TYPE_UNSPECIFIED
}
}
return notification, nil
}
func (s *APIV1Service) convertUserNotificationPayload(ctx context.Context, message *storepb.InboxMessage) (*v1pb.UserNotification_MemoCommentPayload, error) {
func canViewerAccessMemo(viewer *store.User, memo *store.Memo) bool {
if memo == nil {
return false
}
if viewer != nil && isSuperUser(viewer) {
return true
}
if memo.Visibility == store.Private {
return viewer != nil && viewer.ID == memo.CreatorID
}
if memo.Visibility == store.Protected {
return viewer != nil
}
return true
}
func (s *APIV1Service) memoNotificationSnippet(memo *store.Memo) (string, error) {
if memo == nil || memo.Content == "" {
return "", nil
}
snippet, err := s.getMemoContentSnippet(memo.Content)
if err != nil {
return "", err
}
return snippet, nil
}
func (s *APIV1Service) convertMemoCommentNotificationPayload(viewer *store.User, message *storepb.InboxMessage, memosByID map[int32]*store.Memo) (*v1pb.UserNotification_MemoCommentPayload, error) {
memoComment := message.GetMemoComment()
if message == nil || message.Type != storepb.InboxMessage_MEMO_COMMENT || memoComment == nil {
return nil, nil
}
commentMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoComment.MemoId,
ExcludeContent: true,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get comment memo")
commentMemo := memosByID[memoComment.MemoId]
if !canViewerAccessMemo(viewer, commentMemo) {
return nil, nil
}
if commentMemo == nil {
relatedMemo := memosByID[memoComment.RelatedMemoId]
if !canViewerAccessMemo(viewer, relatedMemo) {
return nil, nil
}
relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoComment.RelatedMemoId,
ExcludeContent: true,
})
memoSnippet, err := s.memoNotificationSnippet(commentMemo)
if err != nil {
return nil, errors.Wrap(err, "failed to get related memo")
return nil, errors.Wrap(err, "failed to get comment memo snippet")
}
if relatedMemo == nil {
return nil, nil
relatedMemoSnippet, err := s.memoNotificationSnippet(relatedMemo)
if err != nil {
return nil, errors.Wrap(err, "failed to get related memo snippet")
}
return &v1pb.UserNotification_MemoCommentPayload{
Memo: fmt.Sprintf("%s%s", MemoNamePrefix, commentMemo.UID),
RelatedMemo: fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID),
MemoSnippet: memoSnippet,
RelatedMemoSnippet: relatedMemoSnippet,
}, nil
}
func (s *APIV1Service) convertMemoMentionNotificationPayload(viewer *store.User, message *storepb.InboxMessage, memosByID map[int32]*store.Memo) (*v1pb.UserNotification_MemoMentionPayload, error) {
memoMention := message.GetMemoMention()
if message == nil || message.Type != storepb.InboxMessage_MEMO_MENTION || memoMention == nil {
return nil, nil
}
memo := memosByID[memoMention.MemoId]
if !canViewerAccessMemo(viewer, memo) {
return nil, nil
}
memoSnippet, err := s.memoNotificationSnippet(memo)
if err != nil {
return nil, errors.Wrap(err, "failed to get mention memo snippet")
}
payload := &v1pb.UserNotification_MemoMentionPayload{
Memo: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID),
MemoSnippet: memoSnippet,
}
if memoMention.RelatedMemoId != 0 {
relatedMemo := memosByID[memoMention.RelatedMemoId]
if canViewerAccessMemo(viewer, relatedMemo) {
payload.RelatedMemo = fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID)
relatedMemoSnippet, err := s.memoNotificationSnippet(relatedMemo)
if err != nil {
return nil, errors.Wrap(err, "failed to get related memo snippet")
}
payload.RelatedMemoSnippet = relatedMemoSnippet
}
}
return payload, nil
}
......@@ -39,6 +39,7 @@ type APIV1Service struct {
func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store) *APIV1Service {
markdownService := markdown.NewService(
markdown.WithTagExtension(),
markdown.WithMentionExtension(),
)
return &APIV1Service{
Secret: secret,
......
......@@ -49,7 +49,7 @@ func (r *Runner) RunOnce(ctx context.Context) {
// Process batch
batchSuccessCount := 0
for _, memo := range memos {
if err := RebuildMemoPayload(memo, r.MarkdownService); err != nil {
if err := RebuildMemoPayload(ctx, memo, r.MarkdownService); err != nil {
slog.Error("failed to rebuild memo payload", "err", err, "memoID", memo.ID)
continue
}
......@@ -71,7 +71,7 @@ func (r *Runner) RunOnce(ctx context.Context) {
}
}
func RebuildMemoPayload(memo *store.Memo, markdownService markdown.Service) error {
func RebuildMemoPayload(_ context.Context, memo *store.Memo, markdownService markdown.Service) error {
if memo.Payload == nil {
memo.Payload = &storepb.MemoPayload{}
}
......
......@@ -83,6 +83,7 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
where, args := []string{"1 = 1"}, []any{}
orderBy := []string{"`created_ts` DESC", "`row_status` DESC"}
if len(find.Filters) > 0 {
return nil, errors.Errorf("user filters are not supported")
......@@ -104,6 +105,22 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
return list
}()...)
}
if len(find.UsernameList) > 0 {
placeholders := make([]string, 0, len(find.UsernameList))
for range find.UsernameList {
placeholders = append(placeholders, "?")
}
where, args = append(where, fmt.Sprintf("`username` IN (%s)", strings.Join(placeholders, ", "))), append(args, func() []any {
list := make([]any, 0, len(find.UsernameList))
for _, username := range find.UsernameList {
list = append(list, username)
}
return list
}()...)
}
if v := find.RowStatus; v != nil {
where, args = append(where, "`row_status` = ?"), append(args, *v)
}
if v := find.Username; v != nil {
where, args = append(where, "`username` = ?"), append(args, *v)
}
......@@ -116,8 +133,17 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
if v := find.Nickname; v != nil {
where, args = append(where, "`nickname` = ?"), append(args, *v)
}
orderBy := []string{"`created_ts` DESC", "`row_status` DESC"}
if v := find.Search; v != nil && strings.TrimSpace(*v) != "" {
query := strings.ToLower(strings.TrimSpace(*v))
where, args = append(where, "(LOWER(`username`) LIKE ? OR LOWER(`nickname`) LIKE ?)"), append(args, "%"+query+"%", "%"+query+"%")
orderBy = []string{
"CASE WHEN LOWER(`username`) = ? THEN 0 WHEN LOWER(`username`) LIKE ? THEN 1 WHEN LOWER(`nickname`) LIKE ? THEN 2 ELSE 3 END",
"CHAR_LENGTH(`username`) ASC",
"`created_ts` DESC",
"`row_status` DESC",
}
args = append(args, query, query+"%", query+"%")
}
query := "SELECT `id`, `username`, `role`, `email`, `nickname`, `password_hash`, `avatar_url`, `description`, UNIX_TIMESTAMP(`created_ts`), UNIX_TIMESTAMP(`updated_ts`), `row_status` FROM `user` WHERE " + strings.Join(where, " AND ") + " ORDER BY " + strings.Join(orderBy, ", ")
if v := find.Limit; v != nil {
query += fmt.Sprintf(" LIMIT %d", *v)
......
......@@ -86,6 +86,7 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
where, args := []string{"1 = 1"}, []any{}
orderBy := []string{"created_ts DESC", "row_status DESC"}
if len(find.Filters) > 0 {
return nil, errors.Errorf("user filters are not supported")
......@@ -102,6 +103,17 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
}
where = append(where, fmt.Sprintf("id IN (%s)", strings.Join(holders, ", ")))
}
if len(find.UsernameList) > 0 {
holders := make([]string, 0, len(find.UsernameList))
for _, username := range find.UsernameList {
holders = append(holders, placeholder(len(args)+1))
args = append(args, username)
}
where = append(where, fmt.Sprintf("username IN (%s)", strings.Join(holders, ", ")))
}
if v := find.RowStatus; v != nil {
where, args = append(where, "row_status = "+placeholder(len(args)+1)), append(args, *v)
}
if v := find.Username; v != nil {
where, args = append(where, "username = "+placeholder(len(args)+1)), append(args, *v)
}
......@@ -114,8 +126,19 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
if v := find.Nickname; v != nil {
where, args = append(where, "nickname = "+placeholder(len(args)+1)), append(args, *v)
}
orderBy := []string{"created_ts DESC", "row_status DESC"}
if v := find.Search; v != nil && strings.TrimSpace(*v) != "" {
query := strings.ToLower(strings.TrimSpace(*v))
where, args = append(where, "(LOWER(username) LIKE "+placeholder(len(args)+1)+" OR LOWER(nickname) LIKE "+placeholder(len(args)+2)+")"), append(args, "%"+query+"%", "%"+query+"%")
orderBy = []string{
"CASE WHEN LOWER(username) = " + placeholder(len(args)+1) + " THEN 0 " +
"WHEN LOWER(username) LIKE " + placeholder(len(args)+2) + " THEN 1 " +
"WHEN LOWER(nickname) LIKE " + placeholder(len(args)+3) + " THEN 2 ELSE 3 END",
"LENGTH(username) ASC",
"created_ts DESC",
"row_status DESC",
}
args = append(args, query, query+"%", query+"%")
}
query := `
SELECT
id,
......
......@@ -87,6 +87,7 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
where, args := []string{"1 = 1"}, []any{}
orderBy := []string{"created_ts DESC", "row_status DESC"}
if len(find.Filters) > 0 {
return nil, errors.Errorf("user filters are not supported")
......@@ -108,6 +109,22 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
return list
}()...)
}
if len(find.UsernameList) > 0 {
placeholders := make([]string, 0, len(find.UsernameList))
for range find.UsernameList {
placeholders = append(placeholders, "?")
}
where, args = append(where, fmt.Sprintf("username IN (%s)", strings.Join(placeholders, ", "))), append(args, func() []any {
list := make([]any, 0, len(find.UsernameList))
for _, username := range find.UsernameList {
list = append(list, username)
}
return list
}()...)
}
if v := find.RowStatus; v != nil {
where, args = append(where, "row_status = ?"), append(args, *v)
}
if v := find.Username; v != nil {
where, args = append(where, "username = ?"), append(args, *v)
}
......@@ -120,8 +137,17 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
if v := find.Nickname; v != nil {
where, args = append(where, "nickname = ?"), append(args, *v)
}
orderBy := []string{"created_ts DESC", "row_status DESC"}
if v := find.Search; v != nil && strings.TrimSpace(*v) != "" {
query := strings.ToLower(strings.TrimSpace(*v))
where, args = append(where, "(LOWER(username) LIKE ? OR LOWER(nickname) LIKE ?)"), append(args, "%"+query+"%", "%"+query+"%")
orderBy = []string{
"CASE WHEN LOWER(username) = ? THEN 0 WHEN LOWER(username) LIKE ? THEN 1 WHEN LOWER(nickname) LIKE ? THEN 2 ELSE 3 END",
"LENGTH(username) ASC",
"created_ts DESC",
"row_status DESC",
}
args = append(args, query, query+"%", query+"%")
}
query := `
SELECT
id,
......
......@@ -60,11 +60,14 @@ type FindUser struct {
ID *int32
IDList []int32
UsernameList []string
RowStatus *RowStatus
Username *string
Role *Role
Email *string
Nickname *string
Search *string
// Domain specific fields
Filters []string
......
import { create } from "@bufbuild/protobuf";
import { FieldMaskSchema, timestampDate } from "@bufbuild/protobuf/wkt";
import { CheckIcon, MessageCircleIcon, TrashIcon, XIcon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import UserAvatar from "@/components/UserAvatar";
import { memoServiceClient, userServiceClient } from "@/connect";
import useAsyncEffect from "@/hooks/useAsyncEffect";
import { userServiceClient } from "@/connect";
import useNavigateTo from "@/hooks/useNavigateTo";
import { useUser } from "@/hooks/useUserQueries";
import { handleError } from "@/lib/error";
import { cn } from "@/lib/utils";
import { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
......@@ -21,53 +16,8 @@ interface Props {
function MemoCommentMessage({ notification }: Props) {
const t = useTranslate();
const navigateTo = useNavigateTo();
const [relatedMemo, setRelatedMemo] = useState<Memo | undefined>(undefined);
const [commentMemo, setCommentMemo] = useState<Memo | undefined>(undefined);
const [senderName, setSenderName] = useState<string | undefined>(undefined);
const [initialized, setInitialized] = useState<boolean>(false);
const [hasError, setHasError] = useState<boolean>(false);
const { data: sender } = useUser(senderName || "", { enabled: !!senderName });
useAsyncEffect(async () => {
if (notification.payload?.case !== "memoComment") {
setHasError(true);
return;
}
try {
const memoCommentPayload = notification.payload.value;
const memo = await memoServiceClient.getMemo({
name: memoCommentPayload.relatedMemo,
});
setRelatedMemo(memo);
const comment = await memoServiceClient.getMemo({
name: memoCommentPayload.memo,
});
setCommentMemo(comment);
setSenderName(notification.sender);
setInitialized(true);
} catch (error) {
handleError(error, () => {}, {
context: "Failed to fetch memo comment notification",
onError: () => setHasError(true),
});
return;
}
}, [notification.payload, notification.sender]);
const handleNavigateToMemo = async () => {
if (!relatedMemo) {
return;
}
navigateTo(`/${relatedMemo.name}`);
if (notification.status === UserNotification_Status.UNREAD) {
handleArchiveMessage(true);
}
};
const commentPayload = notification.payload?.case === "memoComment" ? notification.payload.value : undefined;
const sender = notification.senderUser;
const handleArchiveMessage = async (silence = false) => {
await userServiceClient.updateUserNotification({
......@@ -89,22 +39,7 @@ function MemoCommentMessage({ notification }: Props) {
toast.success(t("message.deleted-successfully"));
};
if (!initialized && !hasError) {
return (
<div className="w-full px-5 py-4 border-b border-border/60 last:border-b-0 bg-muted/10 animate-pulse">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-full bg-muted/50 shrink-0" />
<div className="flex-1 space-y-3">
<div className="h-4 bg-muted/50 rounded-md w-2/5" />
<div className="h-3 bg-muted/40 rounded-md w-3/4" />
<div className="h-20 bg-muted/30 rounded-xl" />
</div>
</div>
</div>
);
}
if (hasError) {
if (!commentPayload) {
return (
<div className="w-full px-5 py-4 border-b border-border/60 last:border-b-0 bg-destructive/[0.04] group">
<div className="flex items-center justify-between">
......@@ -128,6 +63,13 @@ function MemoCommentMessage({ notification }: Props) {
const isUnread = notification.status === UserNotification_Status.UNREAD;
const handleNavigateToMemo = async () => {
navigateTo(`/${commentPayload.relatedMemo}`);
if (isUnread) {
await handleArchiveMessage(true);
}
};
return (
<div
className={cn(
......@@ -135,11 +77,9 @@ function MemoCommentMessage({ notification }: Props) {
isUnread ? "bg-primary/[0.03] hover:bg-primary/[0.05]" : "hover:bg-muted/30",
)}
>
{/* Unread indicator bar */}
{isUnread && <div className="absolute left-0 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary to-primary/60" />}
<div className="flex items-start gap-3">
{/* Avatar & Icon */}
<div className="relative shrink-0">
<UserAvatar className="w-10 h-10 ring-1 ring-border/40" avatarUrl={sender?.avatarUrl} />
<div
......@@ -152,9 +92,7 @@ function MemoCommentMessage({ notification }: Props) {
</div>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-center justify-between gap-3 mb-1">
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
<span className="font-semibold text-sm text-foreground/95">{sender?.displayName || sender?.username}</span>
......@@ -188,18 +126,13 @@ function MemoCommentMessage({ notification }: Props) {
</div>
</div>
{/* Original Memo Snippet */}
{relatedMemo && (
<div className="pl-3 border-l-2 border-muted-foreground/20 mb-3">
<p className="text-sm text-foreground/60 line-clamp-1 leading-relaxed">
<span className="text-xs text-muted-foreground/50 font-medium mr-2 uppercase tracking-wide">Original:</span>
{relatedMemo.content || <span className="italic text-muted-foreground/40">Empty memo</span>}
{commentPayload.relatedMemoSnippet || <span className="italic text-muted-foreground/40">Empty memo</span>}
</p>
</div>
)}
{/* Comment Preview */}
{commentMemo && (
<div
onClick={handleNavigateToMemo}
className="p-2 sm:p-3 rounded-lg bg-gradient-to-br from-primary/[0.06] to-primary/[0.03] hover:from-primary/[0.1] hover:to-primary/[0.06] cursor-pointer border border-primary/30 hover:border-primary/50 transition-all duration-200 group/comment shadow-sm hover:shadow"
......@@ -211,12 +144,11 @@ function MemoCommentMessage({ notification }: Props) {
<div className="flex-1 min-w-0">
<p className="text-xs text-primary/60 font-semibold mb-1 uppercase tracking-wider">Comment</p>
<p className="text-sm text-foreground/90 line-clamp-2">
{commentMemo.content || <span className="italic text-muted-foreground/50">Empty comment</span>}
{commentPayload.memoSnippet || <span className="italic text-muted-foreground/50">Empty comment</span>}
</p>
</div>
</div>
</div>
)}
</div>
</div>
</div>
......
import { create } from "@bufbuild/protobuf";
import { FieldMaskSchema, timestampDate } from "@bufbuild/protobuf/wkt";
import { AtSignIcon, CheckIcon, MessageSquareIcon, TrashIcon, XIcon } from "lucide-react";
import toast from "react-hot-toast";
import UserAvatar from "@/components/UserAvatar";
import { userServiceClient } from "@/connect";
import useNavigateTo from "@/hooks/useNavigateTo";
import { cn } from "@/lib/utils";
import { UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
interface Props {
notification: UserNotification;
}
function MemoMentionMessage({ notification }: Props) {
const t = useTranslate();
const navigateTo = useNavigateTo();
const mentionPayload = notification.payload?.case === "memoMention" ? notification.payload.value : undefined;
const sender = notification.senderUser;
const handleArchiveMessage = async (silence = false) => {
await userServiceClient.updateUserNotification({
notification: {
name: notification.name,
status: UserNotification_Status.ARCHIVED,
},
updateMask: create(FieldMaskSchema, { paths: ["status"] }),
});
if (!silence) {
toast.success(t("message.archived-successfully"));
}
};
const handleDeleteMessage = async () => {
await userServiceClient.deleteUserNotification({
name: notification.name,
});
toast.success(t("message.deleted-successfully"));
};
if (!mentionPayload) {
return (
<div className="w-full px-5 py-4 border-b border-border/60 last:border-b-0 bg-destructive/[0.04] group">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-destructive/15 flex items-center justify-center shrink-0 ring-1 ring-destructive/20">
<XIcon className="w-5 h-5 text-destructive" strokeWidth={2} />
</div>
<span className="text-sm text-destructive/80 font-medium">{t("inbox.failed-to-load")}</span>
</div>
<button
onClick={handleDeleteMessage}
className="p-1.5 hover:bg-destructive/15 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100"
title={t("common.delete")}
>
<TrashIcon className="w-4 h-4 text-destructive/70 hover:text-destructive transition-colors" strokeWidth={2} />
</button>
</div>
</div>
);
}
const isUnread = notification.status === UserNotification_Status.UNREAD;
const isCommentMention = Boolean(mentionPayload.relatedMemo);
const targetName = mentionPayload.relatedMemo || mentionPayload.memo;
const handleNavigate = async () => {
navigateTo(`/${targetName}`);
if (isUnread) {
await handleArchiveMessage(true);
}
};
return (
<div
className={cn(
"w-full px-5 py-4 border-b border-border/60 last:border-b-0 transition-all duration-200 group relative",
isUnread ? "bg-primary/[0.03] hover:bg-primary/[0.05]" : "hover:bg-muted/30",
)}
>
{isUnread && <div className="absolute left-0 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary to-primary/60" />}
<div className="flex items-start gap-3">
<div className="relative shrink-0">
<UserAvatar className="w-10 h-10 ring-1 ring-border/40" avatarUrl={sender?.avatarUrl} />
<div
className={cn(
"absolute -bottom-1 -right-1 w-5 h-5 rounded-full border-2 border-background flex items-center justify-center shadow-md transition-all",
isUnread ? "bg-primary text-primary-foreground" : "bg-muted/80 text-muted-foreground",
)}
>
<AtSignIcon className="w-2.5 h-2.5" strokeWidth={2.5} />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-3 mb-1">
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
<span className="font-semibold text-sm text-foreground/95">{sender?.displayName || sender?.username}</span>
<span className="text-sm text-muted-foreground/80">mentioned you {isCommentMention ? "in a comment" : "in a memo"}</span>
<span className="text-xs text-muted-foreground/60">
{notification.createTime &&
timestampDate(notification.createTime)?.toLocaleDateString([], { month: "short", day: "numeric" })}{" "}
at{" "}
{notification.createTime &&
timestampDate(notification.createTime)?.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</span>
</div>
<div className="flex items-center gap-1 shrink-0">
{isUnread ? (
<button
onClick={() => handleArchiveMessage()}
className="p-1.5 hover:bg-primary/10 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100"
title={t("common.archive")}
>
<CheckIcon className="w-4 h-4 text-muted-foreground hover:text-primary transition-colors" strokeWidth={2} />
</button>
) : (
<button
onClick={handleDeleteMessage}
className="p-1.5 hover:bg-destructive/10 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100"
title={t("common.delete")}
>
<TrashIcon className="w-4 h-4 text-muted-foreground hover:text-destructive transition-colors" strokeWidth={2} />
</button>
)}
</div>
</div>
{mentionPayload.relatedMemo && (
<div className="pl-3 border-l-2 border-muted-foreground/20 mb-3">
<p className="text-sm text-foreground/60 line-clamp-1 leading-relaxed">
<span className="text-xs text-muted-foreground/50 font-medium mr-2 uppercase tracking-wide">Memo:</span>
{mentionPayload.relatedMemoSnippet || <span className="italic text-muted-foreground/40">Empty memo</span>}
</p>
</div>
)}
<div
onClick={handleNavigate}
className="p-2 sm:p-3 rounded-lg bg-gradient-to-br from-primary/[0.06] to-primary/[0.03] hover:from-primary/[0.1] hover:to-primary/[0.06] cursor-pointer border border-primary/30 hover:border-primary/50 transition-all duration-200 group/comment shadow-sm hover:shadow"
>
<div className="flex items-start gap-2">
<div className="w-5 h-5 flex items-center justify-center shrink-0">
<MessageSquareIcon className="w-4 h-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-primary/60 font-semibold mb-1 uppercase tracking-wider">
{isCommentMention ? "Comment" : "Memo"}
</p>
<p className="text-sm text-foreground/90 line-clamp-2">
{mentionPayload.memoSnippet || <span className="italic text-muted-foreground/50">Empty memo</span>}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default MemoMentionMessage;
import type { Element } from "hast";
import React from "react";
import { isTagElement, isTaskListItemElement } from "@/types/markdown";
import { isMentionElement, isTagElement, isTaskListItemElement } from "@/types/markdown";
/**
* Creates a conditional component that renders different components
......@@ -33,4 +33,4 @@ export const createConditionalComponent = <P extends Record<string, unknown>>(
};
// Re-export type guards for convenience
export { isTagElement as isTagNode, isTaskListItemElement as isTaskListItemNode };
export { isMentionElement as isMentionNode, isTagElement as isTagNode, isTaskListItemElement as isTaskListItemNode };
import type { Element } from "hast";
import { cn } from "@/lib/utils";
interface MentionProps extends React.HTMLAttributes<HTMLSpanElement> {
node?: Element;
"data-mention"?: string;
children?: React.ReactNode;
resolved?: boolean;
}
export const Mention: React.FC<MentionProps> = ({
"data-mention": dataMention,
children,
className,
node: _node,
resolved = false,
...props
}) => {
const username = dataMention || "";
if (!resolved) {
return (
<span data-mention={username} title={`@${username}`} className={className} {...props}>
{children}
</span>
);
}
return (
<a
href={`/u/${username}`}
className={cn("text-blue-600 underline-offset-2 hover:underline dark:text-blue-400", className)}
data-mention={username}
title={`@${username}`}
{...props}
>
{children}
</a>
);
};
import { createContext, type ReactNode, useContext, useMemo } from "react";
import { useUsersByUsernames } from "@/hooks/useUserQueries";
import { extractMentionUsernames } from "@/utils/remark-plugins/remark-mention";
const MentionResolutionContext = createContext<Set<string> | null>(null);
interface MentionResolutionProviderProps {
contents: string[];
children: ReactNode;
}
export const MentionResolutionProvider = ({ contents, children }: MentionResolutionProviderProps) => {
const mentionUsernames = useMemo(() => Array.from(new Set(contents.flatMap((content) => extractMentionUsernames(content)))), [contents]);
const { data: mentionUsers } = useUsersByUsernames(mentionUsernames);
const resolvedMentionUsernames = useMemo(() => {
if (!mentionUsers) {
return new Set<string>();
}
return new Set(Array.from(mentionUsers.entries()).flatMap(([username, user]) => (user ? [username] : [])));
}, [mentionUsers]);
return <MentionResolutionContext.Provider value={resolvedMentionUsernames}>{children}</MentionResolutionContext.Provider>;
};
export function useResolvedMentionUsernames(usernames: string[]) {
const sharedResolvedMentionUsernames = useContext(MentionResolutionContext);
const shouldUseSharedResolution = sharedResolvedMentionUsernames !== null;
const { data: mentionUsers } = useUsersByUsernames(usernames, { enabled: !shouldUseSharedResolution });
return useMemo(() => {
if (sharedResolvedMentionUsernames) {
return sharedResolvedMentionUsernames;
}
if (!mentionUsers) {
return new Set<string>();
}
return new Set(Array.from(mentionUsers.entries()).flatMap(([username, user]) => (user ? [username] : [])));
}, [sharedResolvedMentionUsernames, mentionUsers]);
}
import type { Element } from "hast";
import { ChevronDown, ChevronUp } from "lucide-react";
import { memo } from "react";
import { memo, useMemo } from "react";
import ReactMarkdown from "react-markdown";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
......@@ -12,18 +12,40 @@ import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n";
import { rehypeHeadingId } from "@/utils/rehype-plugins/rehype-heading-id";
import { remarkDisableSetext } from "@/utils/remark-plugins/remark-disable-setext";
import { extractMentionUsernames, remarkMention } from "@/utils/remark-plugins/remark-mention";
import { remarkPreserveType } from "@/utils/remark-plugins/remark-preserve-type";
import { remarkTag } from "@/utils/remark-plugins/remark-tag";
import { CodeBlock } from "./CodeBlock";
import { isTagNode, isTaskListItemNode } from "./ConditionalComponent";
import { isMentionNode, isTagNode, isTaskListItemNode } from "./ConditionalComponent";
import { COMPACT_MODE_CONFIG, SANITIZE_SCHEMA } from "./constants";
import { useCompactLabel, useCompactMode } from "./hooks";
import { Mention } from "./Mention";
import { useResolvedMentionUsernames } from "./MentionResolutionContext";
import { Blockquote, Heading, HorizontalRule, Image, InlineCode, Link, List, ListItem, Paragraph } from "./markdown";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "./Table";
import { Tag } from "./Tag";
import { TaskListItem } from "./TaskListItem";
import type { MemoContentProps } from "./types";
function getMentionUsername(node: Element, children?: React.ReactNode): string {
const dataMention = node.properties?.["data-mention"];
if (typeof dataMention === "string" && dataMention !== "") {
return dataMention;
}
const camelDataMention = (node.properties as Record<string, unknown> | undefined)?.dataMention;
if (typeof camelDataMention === "string" && camelDataMention !== "") {
return camelDataMention;
}
const text = Array.isArray(children) ? children.join("") : children;
if (typeof text === "string" && text.startsWith("@")) {
return text.slice(1).toLowerCase();
}
return "";
}
const MemoContent = (props: MemoContentProps) => {
const { className, contentClassName, content, onClick, onDoubleClick } = props;
const t = useTranslate();
......@@ -32,6 +54,8 @@ const MemoContent = (props: MemoContentProps) => {
mode: showCompactMode,
toggle: toggleCompactMode,
} = useCompactMode(Boolean(props.compact));
const mentionUsernames = useMemo(() => extractMentionUsernames(content), [content]);
const resolvedMentionUsernames = useResolvedMentionUsernames(mentionUsernames);
const compactLabel = useCompactLabel(showCompactMode, t as (key: string) => string);
......@@ -51,7 +75,7 @@ const MemoContent = (props: MemoContentProps) => {
onDoubleClick={onDoubleClick}
>
<ReactMarkdown
remarkPlugins={[remarkDisableSetext, remarkMath, remarkGfm, remarkBreaks, remarkTag, remarkPreserveType]}
remarkPlugins={[remarkDisableSetext, remarkMath, remarkGfm, remarkBreaks, remarkMention, remarkTag, remarkPreserveType]}
rehypePlugins={[
rehypeRaw,
[rehypeSanitize, SANITIZE_SCHEMA],
......@@ -69,6 +93,10 @@ const MemoContent = (props: MemoContentProps) => {
}) as React.ComponentType<React.ComponentProps<"input">>,
span: ((spanProps: React.ComponentProps<"span"> & { node?: Element }) => {
const { node, ...rest } = spanProps;
if (node && isMentionNode(node)) {
const username = getMentionUsername(node, spanProps.children);
return <Mention {...spanProps} data-mention={username} resolved={resolvedMentionUsernames.has(username)} />;
}
if (node && isTagNode(node)) {
return <Tag {...spanProps} />;
}
......
......@@ -22,6 +22,7 @@ export interface UseSuggestionsReturn<T> {
suggestions: T[];
selectedIndex: number;
isVisible: boolean;
searchQuery: string;
handleItemSelect: (item: T) => void;
}
......@@ -52,12 +53,17 @@ export function useSuggestions<T>({
const hide = () => setPosition(null);
const suggestionsRef = useRef<T[]>([]);
const searchQueryRef = useRef("");
suggestionsRef.current = (() => {
const [word] = getCurrentWord();
if (!word.startsWith(triggerChar)) return [];
const searchQuery = word.slice(triggerChar.length).toLowerCase();
return filterItems(items, searchQuery);
searchQueryRef.current = word.slice(triggerChar.length).toLowerCase();
return filterItems(items, searchQueryRef.current);
})();
if (suggestionsRef.current.length === 0) {
const [word] = getCurrentWord();
searchQueryRef.current = word.startsWith(triggerChar) ? word.slice(triggerChar.length).toLowerCase() : "";
}
const isVisibleRef = useRef(false);
isVisibleRef.current = !!(position && suggestionsRef.current.length > 0);
......@@ -153,6 +159,7 @@ export function useSuggestions<T>({
suggestions: suggestionsRef.current,
selectedIndex,
isVisible: isVisibleRef.current,
searchQuery: searchQueryRef.current,
handleItemSelect: handleAutocomplete,
};
}
......@@ -2,6 +2,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { ArrowUpIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { matchPath } from "react-router-dom";
import { MentionResolutionProvider } from "@/components/MemoContent/MentionResolutionContext";
import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
......@@ -145,6 +146,7 @@ const PagedMemoList = (props: Props) => {
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const children = (
<MentionResolutionProvider contents={sortedMemoList.map((memo) => memo.content)}>
<div className="flex flex-col justify-start w-full max-w-2xl mx-auto">
{/* Show skeleton loader during initial load */}
{isLoading ? (
......@@ -176,6 +178,7 @@ const PagedMemoList = (props: Props) => {
</>
)}
</div>
</MentionResolutionProvider>
);
return children;
......
......@@ -6,6 +6,8 @@ import { buildUserSettingName } from "@/helpers/resource-names";
import useCurrentUser from "@/hooks/useCurrentUser";
import { User, UserSetting, UserSetting_GeneralSetting, UserSetting_Key, UserSettingSchema } from "@/types/proto/api/v1/user_service_pb";
const BATCH_GET_USERS_LIMIT = 100;
// Query keys factory
export const userKeys = {
all: ["users"] as const,
......@@ -16,7 +18,8 @@ export const userKeys = {
currentUser: () => [...userKeys.all, "current"] as const,
shortcuts: () => [...userKeys.all, "shortcuts"] as const,
notifications: () => [...userKeys.all, "notifications"] as const,
byNames: (names: string[]) => [...userKeys.all, "byNames", ...names.sort()] as const,
byNames: (names: string[]) => [...userKeys.all, "byNames", ...[...names].sort()] as const,
byUsernames: (usernames: string[]) => [...userKeys.all, "byUsernames", ...[...usernames].sort()] as const,
};
export function useUser(name: string, options?: { enabled?: boolean }) {
......@@ -244,3 +247,30 @@ export function useUsersByNames(names: string[]) {
staleTime: 1000 * 60 * 5, // 5 minutes - user profiles don't change often
});
}
// Hook to fetch multiple users by usernames (returns Map<username, User>)
export function useUsersByUsernames(usernames: string[], options?: { enabled?: boolean }) {
const enabled = (options?.enabled ?? true) && usernames.length > 0;
const uniqueUsernames = Array.from(new Set(usernames));
return useQuery({
queryKey: userKeys.byUsernames(uniqueUsernames),
queryFn: async () => {
const batches = [];
for (let i = 0; i < uniqueUsernames.length; i += BATCH_GET_USERS_LIMIT) {
batches.push(uniqueUsernames.slice(i, i + BATCH_GET_USERS_LIMIT));
}
const responses = await Promise.all(batches.map((batch) => userServiceClient.batchGetUsers({ usernames: batch })));
const usersByUsername = new Map(responses.flatMap((response) => response.users).map((user) => [user.username, user] as const));
const userMap = new Map<string, User | undefined>();
for (const username of uniqueUsernames) {
userMap.set(username, usersByUsername.get(username));
}
return userMap;
},
enabled,
staleTime: 1000 * 60 * 5,
});
}
......@@ -4,6 +4,7 @@ import { ArchiveIcon, BellIcon, InboxIcon } from "lucide-react";
import { useState } from "react";
import Empty from "@/components/Empty";
import MemoCommentMessage from "@/components/Inbox/MemoCommentMessage";
import MemoMentionMessage from "@/components/Inbox/MemoMentionMessage";
import MobileHeader from "@/components/MobileHeader";
import useMediaQuery from "@/hooks/useMediaQuery";
import { useNotifications } from "@/hooks/useUserQueries";
......@@ -108,6 +109,9 @@ const Inboxes = () => {
if (notification.type === UserNotification_Type.MEMO_COMMENT) {
return <MemoCommentMessage key={notification.name} notification={notification} />;
}
if (notification.type === UserNotification_Type.MEMO_MENTION) {
return <MemoMentionMessage key={notification.name} notification={notification} />;
}
return null;
})}
</div>
......
......@@ -3,6 +3,7 @@ import { ArrowUpLeftFromCircleIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, Navigate, useLocation, useParams } from "react-router-dom";
import MemoCommentSection from "@/components/MemoCommentSection";
import { MentionResolutionProvider } from "@/components/MemoContent/MentionResolutionContext";
import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
import MemoView from "@/components/MemoView";
import MobileHeader from "@/components/MobileHeader";
......@@ -73,6 +74,7 @@ const MemoDetail = () => {
const displayMemo = isShareMode
? { ...memo, attachments: withShareAttachmentLinks(memo.attachments as Attachment[], shareToken!) }
: memo;
const mentionResolutionContents = [displayMemo.content, ...comments.map((comment) => comment.content)];
return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
......@@ -81,6 +83,7 @@ const MemoDetail = () => {
<MemoDetailSidebarDrawer memo={displayMemo} onShareImageOpen={() => setShareImageDialogOpen(true)} />
</MobileHeader>
)}
<MentionResolutionProvider contents={mentionResolutionContents}>
<div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
<div className={cn("w-full md:w-[calc(100%-15rem)]")}>
{parentMemo && (
......@@ -115,6 +118,7 @@ const MemoDetail = () => {
</div>
)}
</div>
</MentionResolutionProvider>
</section>
);
};
......
......@@ -6,17 +6,34 @@ export interface TagNode {
data: TagNodeData;
}
export interface MentionNode {
type: "mentionNode";
value: string;
data: MentionNodeData;
}
export interface TagNodeData {
hName: "span";
hProperties: TagNodeProperties;
hChildren: Array<{ type: "text"; value: string }>;
}
export interface MentionNodeData {
hName: "span";
hProperties: MentionNodeProperties;
hChildren: Array<{ type: "text"; value: string }>;
}
export interface TagNodeProperties {
className: string;
"data-tag": string;
}
export interface MentionNodeProperties {
className: string;
"data-mention": string;
}
export interface ExtendedData extends Data {
mdastType?: string;
}
......@@ -30,10 +47,39 @@ export function isTagElement(node: HastElement): boolean {
return true;
}
const dataTag = node.properties?.["data-tag"];
if (typeof dataTag === "string" && dataTag !== "") {
return true;
}
const className = node.properties?.className;
if (Array.isArray(className) && className.includes("tag")) {
return true;
}
if (typeof className === "string" && className.split(/\s+/).includes("tag")) {
return true;
}
return false;
}
export function isMentionElement(node: HastElement): boolean {
if (hasExtendedData(node) && node.data.mdastType === "mentionNode") {
return true;
}
const dataMention = node.properties?.["data-mention"];
if (typeof dataMention === "string" && dataMention !== "") {
return true;
}
const className = node.properties?.className;
if (Array.isArray(className) && className.includes("mention")) {
return true;
}
if (typeof className === "string" && className.split(/\s+/).includes("mention")) {
return true;
}
return false;
}
......
......@@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file api/v1/user_service.proto.
*/
export const file_api_v1_user_service: GenFile = /*@__PURE__*/
fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJjChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIm0KDkdldFVzZXJSZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISMgoJcmVhZF9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EEBIogBChFDcmVhdGVVc2VyUmVxdWVzdBIoCgR1c2VyGAEgASgLMhIubWVtb3MuYXBpLnYxLlVzZXJCBuBBAuBBBBIUCgd1c2VyX2lkGAIgASgJQgPgQQESGgoNdmFsaWRhdGVfb25seRgDIAEoCEID4EEBEhcKCnJlcXVlc3RfaWQYBCABKAlCA+BBASKMAQoRVXBkYXRlVXNlclJlcXVlc3QSJQoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQISGgoNYWxsb3dfbWlzc2luZxgDIAEoCEID4EEBIlAKEURlbGV0ZVVzZXJSZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISEgoFZm9yY2UYAiABKAhCA+BBASLYAwoJVXNlclN0YXRzEhEKBG5hbWUYASABKAlCA+BBCBI7ChdtZW1vX2Rpc3BsYXlfdGltZXN0YW1wcxgCIAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASPgoPbWVtb190eXBlX3N0YXRzGAMgASgLMiUubWVtb3MuYXBpLnYxLlVzZXJTdGF0cy5NZW1vVHlwZVN0YXRzEjgKCXRhZ19jb3VudBgEIAMoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuVGFnQ291bnRFbnRyeRIUCgxwaW5uZWRfbWVtb3MYBSADKAkSGAoQdG90YWxfbWVtb19jb3VudBgGIAEoBRovCg1UYWdDb3VudEVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEaXwoNTWVtb1R5cGVTdGF0cxISCgpsaW5rX2NvdW50GAEgASgFEhIKCmNvZGVfY291bnQYAiABKAUSEgoKdG9kb19jb3VudBgDIAEoBRISCgp1bmRvX2NvdW50GAQgASgFOj/qQTwKFm1lbW9zLmFwaS52MS9Vc2VyU3RhdHMSDHVzZXJzL3t1c2VyfSoJdXNlclN0YXRzMgl1c2VyU3RhdHMiPgoTR2V0VXNlclN0YXRzUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyIhkKF0xpc3RBbGxVc2VyU3RhdHNSZXF1ZXN0IkIKGExpc3RBbGxVc2VyU3RhdHNSZXNwb25zZRImCgVzdGF0cxgBIAMoCzIXLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMi5AMKC1VzZXJTZXR0aW5nEhEKBG5hbWUYASABKAlCA+BBCBJDCg9nZW5lcmFsX3NldHRpbmcYAiABKAsyKC5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcuR2VuZXJhbFNldHRpbmdIABJFChB3ZWJob29rc19zZXR0aW5nGAUgASgLMikubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLldlYmhvb2tzU2V0dGluZ0gAGlcKDkdlbmVyYWxTZXR0aW5nEhMKBmxvY2FsZRgBIAEoCUID4EEBEhwKD21lbW9fdmlzaWJpbGl0eRgDIAEoCUID4EEBEhIKBXRoZW1lGAQgASgJQgPgQQEaPgoPV2ViaG9va3NTZXR0aW5nEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIjUKA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESDAoIV0VCSE9PS1MQBDpd6kFaChhtZW1vcy5hcGkudjEvVXNlclNldHRpbmcSI3VzZXJzL3t1c2VybmFtZX0vc2V0dGluZ3Mve3NldHRpbmd9Kgx1c2VyU2V0dGluZ3MyC3VzZXJTZXR0aW5nQgcKBXZhbHVlIkcKFUdldFVzZXJTZXR0aW5nUmVxdWVzdBIuCgRuYW1lGAEgASgJQiDgQQL6QRoKGG1lbW9zLmFwaS52MS9Vc2VyU2V0dGluZyKBAQoYVXBkYXRlVXNlclNldHRpbmdSZXF1ZXN0Ei8KB3NldHRpbmcYASABKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmdCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAiJ1ChdMaXN0VXNlclNldHRpbmdzUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBInQKGExpc3RVc2VyU2V0dGluZ3NSZXNwb25zZRIrCghzZXR0aW5ncxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZxIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEgoKdG90YWxfc2l6ZRgDIAEoBSLyAgoTUGVyc29uYWxBY2Nlc3NUb2tlbhIRCgRuYW1lGAEgASgJQgPgQQgSGAoLZGVzY3JpcHRpb24YAiABKAlCA+BBARIzCgpjcmVhdGVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjMKCmV4cGlyZXNfYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQESNQoMbGFzdF91c2VkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDOowB6kGIAQogbWVtb3MuYXBpLnYxL1BlcnNvbmFsQWNjZXNzVG9rZW4SOXVzZXJzL3t1c2VyfS9wZXJzb25hbEFjY2Vzc1Rva2Vucy97cGVyc29uYWxfYWNjZXNzX3Rva2VufSoUcGVyc29uYWxBY2Nlc3NUb2tlbnMyE3BlcnNvbmFsQWNjZXNzVG9rZW4ifQofTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBIpIBCiBMaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXNwb25zZRJBChZwZXJzb25hbF9hY2Nlc3NfdG9rZW5zGAEgAygLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUihQEKIENyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIYCgtkZXNjcmlwdGlvbhgCIAEoCUID4EEBEhwKD2V4cGlyZXNfaW5fZGF5cxgDIAEoBUID4EEBInQKIUNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZRJAChVwZXJzb25hbF9hY2Nlc3NfdG9rZW4YASABKAsyIS5tZW1vcy5hcGkudjEuUGVyc29uYWxBY2Nlc3NUb2tlbhINCgV0b2tlbhgCIAEoCSJaCiBEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBI2CgRuYW1lGAEgASgJQijgQQL6QSIKIG1lbW9zLmFwaS52MS9QZXJzb25hbEFjY2Vzc1Rva2VuIqoBCgtVc2VyV2ViaG9vaxIMCgRuYW1lGAEgASgJEgsKA3VybBgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNAoLdXBkYXRlX3RpbWUYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMiLgoXTGlzdFVzZXJXZWJob29rc1JlcXVlc3QSEwoGcGFyZW50GAEgASgJQgPgQQIiRwoYTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rImAKGENyZWF0ZVVzZXJXZWJob29rUmVxdWVzdBITCgZwYXJlbnQYASABKAlCA+BBAhIvCgd3ZWJob29rGAIgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rQgPgQQIifAoYVXBkYXRlVXNlcldlYmhvb2tSZXF1ZXN0Ei8KB3dlYmhvb2sYASABKAsyGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2tCA+BBAhIvCgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2siLQoYRGVsZXRlVXNlcldlYmhvb2tSZXF1ZXN0EhEKBG5hbWUYASABKAlCA+BBAiLwBAoQVXNlck5vdGlmaWNhdGlvbhIUCgRuYW1lGAEgASgJQgbgQQPgQQgSKQoGc2VuZGVyGAIgASgJQhngQQP6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjoKBnN0YXR1cxgDIAEoDjIlLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLlN0YXR1c0ID4EEBEjQKC2NyZWF0ZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjYKBHR5cGUYBSABKA4yIy5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5UeXBlQgPgQQMSTgoMbWVtb19jb21tZW50GAYgASgLMjEubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uTWVtb0NvbW1lbnRQYXlsb2FkQgPgQQNIABo4ChJNZW1vQ29tbWVudFBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkiOgoGU3RhdHVzEhYKElNUQVRVU19VTlNQRUNJRklFRBAAEgoKBlVOUkVBRBABEgwKCEFSQ0hJVkVEEAIiLgoEVHlwZRIUChBUWVBFX1VOU1BFQ0lGSUVEEAASEAoMTUVNT19DT01NRU5UEAE6cOpBbQodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24SKXVzZXJzL3t1c2VyfS9ub3RpZmljYXRpb25zL3tub3RpZmljYXRpb259GgRuYW1lKg1ub3RpZmljYXRpb25zMgxub3RpZmljYXRpb25CCQoHcGF5bG9hZCKPAQocTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBEhMKBmZpbHRlchgEIAEoCUID4EEBIm8KHUxpc3RVc2VyTm90aWZpY2F0aW9uc1Jlc3BvbnNlEjUKDW5vdGlmaWNhdGlvbnMYASADKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikAEKHVVwZGF0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjkKDG5vdGlmaWNhdGlvbhgBIAEoCzIeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIiVAodRGVsZXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QSMwoEbmFtZRgBIAEoCUIl4EEC+kEfCh1tZW1vcy5hcGkudjEvVXNlck5vdGlmaWNhdGlvbjKDFwoLVXNlclNlcnZpY2USYwoJTGlzdFVzZXJzEh4ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1JlcXVlc3QaHy5tZW1vcy5hcGkudjEuTGlzdFVzZXJzUmVzcG9uc2UiFYLT5JMCDxINL2FwaS92MS91c2VycxJiCgdHZXRVc2VyEhwubWVtb3MuYXBpLnYxLkdldFVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiJdpBBG5hbWWC0+STAhgSFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SZQoKQ3JlYXRlVXNlchIfLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIiLaQQR1c2VygtPkkwIVOgR1c2VyIg0vYXBpL3YxL3VzZXJzEn8KClVwZGF0ZVVzZXISHy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciI82kEQdXNlcix1cGRhdGVfbWFza4LT5JMCIzoEdXNlcjIbL2FwaS92MS97dXNlci5uYW1lPXVzZXJzLyp9EmwKCkRlbGV0ZVVzZXISHy5tZW1vcy5hcGkudjEuRGVsZXRlVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiJdpBBG5hbWWC0+STAhgqFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SfgoQTGlzdEFsbFVzZXJTdGF0cxIlLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVzcG9uc2UiG4LT5JMCFRITL2FwaS92MS91c2VyczpzdGF0cxJ6CgxHZXRVc2VyU3RhdHMSIS5tZW1vcy5hcGkudjEuR2V0VXNlclN0YXRzUmVxdWVzdBoXLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMiLtpBBG5hbWWC0+STAiESHy9hcGkvdjEve25hbWU9dXNlcnMvKn06Z2V0U3RhdHMSggEKDkdldFVzZXJTZXR0aW5nEiMubWVtb3MuYXBpLnYxLkdldFVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyIw2kEEbmFtZYLT5JMCIxIhL2FwaS92MS97bmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EqgBChFVcGRhdGVVc2VyU2V0dGluZxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyU2V0dGluZ1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmciUNpBE3NldHRpbmcsdXBkYXRlX21hc2uC0+STAjQ6B3NldHRpbmcyKS9hcGkvdjEve3NldHRpbmcubmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EpUBChBMaXN0VXNlclNldHRpbmdzEiUubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXNwb25zZSIy2kEGcGFyZW50gtPkkwIjEiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vc2V0dGluZ3MSuQEKGExpc3RQZXJzb25hbEFjY2Vzc1Rva2VucxItLm1lbW9zLmFwaS52MS5MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXF1ZXN0Gi4ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1Jlc3BvbnNlIj7aQQZwYXJlbnSC0+STAi8SLS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9wZXJzb25hbEFjY2Vzc1Rva2VucxK2AQoZQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlbhIuLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBovLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2UiOILT5JMCMjoBKiItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zEqEBChlEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkRlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjzaQQRuYW1lgtPkkwIvKi0vYXBpL3YxL3tuYW1lPXVzZXJzLyovcGVyc29uYWxBY2Nlc3NUb2tlbnMvKn0SlQEKEExpc3RVc2VyV2ViaG9va3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS93ZWJob29rcxKbAQoRQ3JlYXRlVXNlcldlYmhvb2sSJi5tZW1vcy5hcGkudjEuQ3JlYXRlVXNlcldlYmhvb2tSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIkPaQQ5wYXJlbnQsd2ViaG9va4LT5JMCLDoHd2ViaG9vayIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEqgBChFVcGRhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siUNpBE3dlYmhvb2ssdXBkYXRlX21hc2uC0+STAjQ6B3dlYmhvb2syKS9hcGkvdjEve3dlYmhvb2submFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EoUBChFEZWxldGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyV2ViaG9va1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiMNpBBG5hbWWC0+STAiMqIS9hcGkvdjEve25hbWU9dXNlcnMvKi93ZWJob29rcy8qfRKpAQoVTGlzdFVzZXJOb3RpZmljYXRpb25zEioubWVtb3MuYXBpLnYxLkxpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QaKy5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVzcG9uc2UiN9pBBnBhcmVudILT5JMCKBImL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L25vdGlmaWNhdGlvbnMSywEKFlVwZGF0ZVVzZXJOb3RpZmljYXRpb24SKy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QaHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbiJk2kEYbm90aWZpY2F0aW9uLHVwZGF0ZV9tYXNrgtPkkwJDOgxub3RpZmljYXRpb24yMy9hcGkvdjEve25vdGlmaWNhdGlvbi5uYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfRKUAQoWRGVsZXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSI12kEEbmFtZYLT5JMCKComL2FwaS92MS97bmFtZT11c2Vycy8qL25vdGlmaWNhdGlvbnMvKn1CqAEKEGNvbS5tZW1vcy5hcGkudjFCEFVzZXJTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);
fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJjChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIikKFEJhdGNoR2V0VXNlcnNSZXF1ZXN0EhEKCXVzZXJuYW1lcxgBIAMoCSI6ChVCYXRjaEdldFVzZXJzUmVzcG9uc2USIQoFdXNlcnMYASADKAsyEi5tZW1vcy5hcGkudjEuVXNlciJtCg5HZXRVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjIKCXJlYWRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBASKIAQoRQ3JlYXRlVXNlclJlcXVlc3QSKAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgbgQQLgQQQSFAoHdXNlcl9pZBgCIAEoCUID4EEBEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBARIXCgpyZXF1ZXN0X2lkGAQgASgJQgPgQQEijAEKEVVwZGF0ZVVzZXJSZXF1ZXN0EiUKBHVzZXIYASABKAsyEi5tZW1vcy5hcGkudjEuVXNlckID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECEhoKDWFsbG93X21pc3NpbmcYAyABKAhCA+BBASJQChFEZWxldGVVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhIKBWZvcmNlGAIgASgIQgPgQQEi2AMKCVVzZXJTdGF0cxIRCgRuYW1lGAEgASgJQgPgQQgSOwoXbWVtb19kaXNwbGF5X3RpbWVzdGFtcHMYAiADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEj4KD21lbW9fdHlwZV9zdGF0cxgDIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuTWVtb1R5cGVTdGF0cxI4Cgl0YWdfY291bnQYBCADKAsyJS5tZW1vcy5hcGkudjEuVXNlclN0YXRzLlRhZ0NvdW50RW50cnkSFAoMcGlubmVkX21lbW9zGAUgAygJEhgKEHRvdGFsX21lbW9fY291bnQYBiABKAUaLwoNVGFnQ291bnRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGl8KDU1lbW9UeXBlU3RhdHMSEgoKbGlua19jb3VudBgBIAEoBRISCgpjb2RlX2NvdW50GAIgASgFEhIKCnRvZG9fY291bnQYAyABKAUSEgoKdW5kb19jb3VudBgEIAEoBTo/6kE8ChZtZW1vcy5hcGkudjEvVXNlclN0YXRzEgx1c2Vycy97dXNlcn0qCXVzZXJTdGF0czIJdXNlclN0YXRzIj4KE0dldFVzZXJTdGF0c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlciIZChdMaXN0QWxsVXNlclN0YXRzUmVxdWVzdCJCChhMaXN0QWxsVXNlclN0YXRzUmVzcG9uc2USJgoFc3RhdHMYASADKAsyFy5tZW1vcy5hcGkudjEuVXNlclN0YXRzIuQDCgtVc2VyU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSQwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMigubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLkdlbmVyYWxTZXR0aW5nSAASRQoQd2ViaG9va3Nfc2V0dGluZxgFIAEoCzIpLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5XZWJob29rc1NldHRpbmdIABpXCg5HZW5lcmFsU2V0dGluZxITCgZsb2NhbGUYASABKAlCA+BBARIcCg9tZW1vX3Zpc2liaWxpdHkYAyABKAlCA+BBARISCgV0aGVtZRgEIAEoCUID4EEBGj4KD1dlYmhvb2tzU2V0dGluZxIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayI1CgNLZXkSEwoPS0VZX1VOU1BFQ0lGSUVEEAASCwoHR0VORVJBTBABEgwKCFdFQkhPT0tTEAQ6XepBWgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nEiN1c2Vycy97dXNlcm5hbWV9L3NldHRpbmdzL3tzZXR0aW5nfSoMdXNlclNldHRpbmdzMgt1c2VyU2V0dGluZ0IHCgV2YWx1ZSJHChVHZXRVc2VyU2V0dGluZ1JlcXVlc3QSLgoEbmFtZRgBIAEoCUIg4EEC+kEaChhtZW1vcy5hcGkudjEvVXNlclNldHRpbmcigQEKGFVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBIvCgdzZXR0aW5nGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIidQoXTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASJ0ChhMaXN0VXNlclNldHRpbmdzUmVzcG9uc2USKwoIc2V0dGluZ3MYASADKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUi8gIKE1BlcnNvbmFsQWNjZXNzVG9rZW4SEQoEbmFtZRgBIAEoCUID4EEIEhgKC2Rlc2NyaXB0aW9uGAIgASgJQgPgQQESMwoKY3JlYXRlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxIzCgpleHBpcmVzX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEBEjUKDGxhc3RfdXNlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAzqMAepBiAEKIG1lbW9zLmFwaS52MS9QZXJzb25hbEFjY2Vzc1Rva2VuEjl1c2Vycy97dXNlcn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMve3BlcnNvbmFsX2FjY2Vzc190b2tlbn0qFHBlcnNvbmFsQWNjZXNzVG9rZW5zMhNwZXJzb25hbEFjY2Vzc1Rva2VuIn0KH0xpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASKSAQogTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2USQQoWcGVyc29uYWxfYWNjZXNzX3Rva2VucxgBIAMoCzIhLm1lbW9zLmFwaS52MS5QZXJzb25hbEFjY2Vzc1Rva2VuEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIoUBCiBDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISGAoLZGVzY3JpcHRpb24YAiABKAlCA+BBARIcCg9leHBpcmVzX2luX2RheXMYAyABKAVCA+BBASJ0CiFDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2USQAoVcGVyc29uYWxfYWNjZXNzX3Rva2VuGAEgASgLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SDQoFdG9rZW4YAiABKAkiWgogRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QSNgoEbmFtZRgBIAEoCUIo4EEC+kEiCiBtZW1vcy5hcGkudjEvUGVyc29uYWxBY2Nlc3NUb2tlbiKqAQoLVXNlcldlYmhvb2sSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEjQKC2NyZWF0ZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjQKC3VwZGF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDIi4KF0xpc3RVc2VyV2ViaG9va3NSZXF1ZXN0EhMKBnBhcmVudBgBIAEoCUID4EECIkcKGExpc3RVc2VyV2ViaG9va3NSZXNwb25zZRIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayJgChhDcmVhdGVVc2VyV2ViaG9va1JlcXVlc3QSEwoGcGFyZW50GAEgASgJQgPgQQISLwoHd2ViaG9vaxgCIAEoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9va0ID4EECInwKGFVwZGF0ZVVzZXJXZWJob29rUmVxdWVzdBIvCgd3ZWJob29rGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rQgPgQQISLwoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrIi0KGERlbGV0ZVVzZXJXZWJob29rUmVxdWVzdBIRCgRuYW1lGAEgASgJQgPgQQIiogcKEFVzZXJOb3RpZmljYXRpb24SFAoEbmFtZRgBIAEoCUIG4EED4EEIEikKBnNlbmRlchgCIAEoCUIZ4EED+kETChFtZW1vcy5hcGkudjEvVXNlchIsCgtzZW5kZXJfdXNlchgIIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgPgQQMSOgoGc3RhdHVzGAMgASgOMiUubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uU3RhdHVzQgPgQQESNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNgoEdHlwZRgFIAEoDjIjLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLlR5cGVCA+BBAxJOCgxtZW1vX2NvbW1lbnQYBiABKAsyMS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5NZW1vQ29tbWVudFBheWxvYWRCA+BBA0gAEk4KDG1lbW9fbWVudGlvbhgHIAEoCzIxLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLk1lbW9NZW50aW9uUGF5bG9hZEID4EEDSAAabAoSTWVtb0NvbW1lbnRQYXlsb2FkEgwKBG1lbW8YASABKAkSFAoMcmVsYXRlZF9tZW1vGAIgASgJEhQKDG1lbW9fc25pcHBldBgDIAEoCRIcChRyZWxhdGVkX21lbW9fc25pcHBldBgEIAEoCRpsChJNZW1vTWVudGlvblBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkSFAoMbWVtb19zbmlwcGV0GAMgASgJEhwKFHJlbGF0ZWRfbWVtb19zbmlwcGV0GAQgASgJIjoKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABIKCgZVTlJFQUQQARIMCghBUkNISVZFRBACIkAKBFR5cGUSFAoQVFlQRV9VTlNQRUNJRklFRBAAEhAKDE1FTU9fQ09NTUVOVBABEhAKDE1FTU9fTUVOVElPThACOnDqQW0KHW1lbW9zLmFwaS52MS9Vc2VyTm90aWZpY2F0aW9uEil1c2Vycy97dXNlcn0vbm90aWZpY2F0aW9ucy97bm90aWZpY2F0aW9ufRoEbmFtZSoNbm90aWZpY2F0aW9uczIMbm90aWZpY2F0aW9uQgkKB3BheWxvYWQijwEKHExpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBARITCgZmaWx0ZXIYBCABKAlCA+BBASJvCh1MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZRI1Cg1ub3RpZmljYXRpb25zGAEgAygLMh4ubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpABCh1VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBI5Cgxub3RpZmljYXRpb24YASABKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbkID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECIlQKHURlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjMKBG5hbWUYASABKAlCJeBBAvpBHwodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24ygBgKC1VzZXJTZXJ2aWNlEmMKCUxpc3RVc2VycxIeLm1lbW9zLmFwaS52MS5MaXN0VXNlcnNSZXF1ZXN0Gh8ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1Jlc3BvbnNlIhWC0+STAg8SDS9hcGkvdjEvdXNlcnMSewoNQmF0Y2hHZXRVc2VycxIiLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVxdWVzdBojLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVzcG9uc2UiIYLT5JMCGzoBKiIWL2FwaS92MS91c2VyczpiYXRjaEdldBJiCgdHZXRVc2VyEhwubWVtb3MuYXBpLnYxLkdldFVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiJdpBBG5hbWWC0+STAhgSFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SZQoKQ3JlYXRlVXNlchIfLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIiLaQQR1c2VygtPkkwIVOgR1c2VyIg0vYXBpL3YxL3VzZXJzEn8KClVwZGF0ZVVzZXISHy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciI82kEQdXNlcix1cGRhdGVfbWFza4LT5JMCIzoEdXNlcjIbL2FwaS92MS97dXNlci5uYW1lPXVzZXJzLyp9EmwKCkRlbGV0ZVVzZXISHy5tZW1vcy5hcGkudjEuRGVsZXRlVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiJdpBBG5hbWWC0+STAhgqFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SfgoQTGlzdEFsbFVzZXJTdGF0cxIlLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVzcG9uc2UiG4LT5JMCFRITL2FwaS92MS91c2VyczpzdGF0cxJ6CgxHZXRVc2VyU3RhdHMSIS5tZW1vcy5hcGkudjEuR2V0VXNlclN0YXRzUmVxdWVzdBoXLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMiLtpBBG5hbWWC0+STAiESHy9hcGkvdjEve25hbWU9dXNlcnMvKn06Z2V0U3RhdHMSggEKDkdldFVzZXJTZXR0aW5nEiMubWVtb3MuYXBpLnYxLkdldFVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyIw2kEEbmFtZYLT5JMCIxIhL2FwaS92MS97bmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EqgBChFVcGRhdGVVc2VyU2V0dGluZxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyU2V0dGluZ1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmciUNpBE3NldHRpbmcsdXBkYXRlX21hc2uC0+STAjQ6B3NldHRpbmcyKS9hcGkvdjEve3NldHRpbmcubmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EpUBChBMaXN0VXNlclNldHRpbmdzEiUubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXNwb25zZSIy2kEGcGFyZW50gtPkkwIjEiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vc2V0dGluZ3MSuQEKGExpc3RQZXJzb25hbEFjY2Vzc1Rva2VucxItLm1lbW9zLmFwaS52MS5MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXF1ZXN0Gi4ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1Jlc3BvbnNlIj7aQQZwYXJlbnSC0+STAi8SLS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9wZXJzb25hbEFjY2Vzc1Rva2VucxK2AQoZQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlbhIuLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBovLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2UiOILT5JMCMjoBKiItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zEqEBChlEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkRlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjzaQQRuYW1lgtPkkwIvKi0vYXBpL3YxL3tuYW1lPXVzZXJzLyovcGVyc29uYWxBY2Nlc3NUb2tlbnMvKn0SlQEKEExpc3RVc2VyV2ViaG9va3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS93ZWJob29rcxKbAQoRQ3JlYXRlVXNlcldlYmhvb2sSJi5tZW1vcy5hcGkudjEuQ3JlYXRlVXNlcldlYmhvb2tSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIkPaQQ5wYXJlbnQsd2ViaG9va4LT5JMCLDoHd2ViaG9vayIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEqgBChFVcGRhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siUNpBE3dlYmhvb2ssdXBkYXRlX21hc2uC0+STAjQ6B3dlYmhvb2syKS9hcGkvdjEve3dlYmhvb2submFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EoUBChFEZWxldGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyV2ViaG9va1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiMNpBBG5hbWWC0+STAiMqIS9hcGkvdjEve25hbWU9dXNlcnMvKi93ZWJob29rcy8qfRKpAQoVTGlzdFVzZXJOb3RpZmljYXRpb25zEioubWVtb3MuYXBpLnYxLkxpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QaKy5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVzcG9uc2UiN9pBBnBhcmVudILT5JMCKBImL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L25vdGlmaWNhdGlvbnMSywEKFlVwZGF0ZVVzZXJOb3RpZmljYXRpb24SKy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QaHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbiJk2kEYbm90aWZpY2F0aW9uLHVwZGF0ZV9tYXNrgtPkkwJDOgxub3RpZmljYXRpb24yMy9hcGkvdjEve25vdGlmaWNhdGlvbi5uYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfRKUAQoWRGVsZXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSI12kEEbmFtZYLT5JMCKComL2FwaS92MS97bmFtZT11c2Vycy8qL25vdGlmaWNhdGlvbnMvKn1CqAEKEGNvbS5tZW1vcy5hcGkudjFCEFVzZXJTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);
/**
* @generated from message memos.api.v1.User
......@@ -223,6 +223,40 @@ export type ListUsersResponse = Message<"memos.api.v1.ListUsersResponse"> & {
export const ListUsersResponseSchema: GenMessage<ListUsersResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 2);
/**
* @generated from message memos.api.v1.BatchGetUsersRequest
*/
export type BatchGetUsersRequest = Message<"memos.api.v1.BatchGetUsersRequest"> & {
/**
* @generated from field: repeated string usernames = 1;
*/
usernames: string[];
};
/**
* Describes the message memos.api.v1.BatchGetUsersRequest.
* Use `create(BatchGetUsersRequestSchema)` to create a new message.
*/
export const BatchGetUsersRequestSchema: GenMessage<BatchGetUsersRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 3);
/**
* @generated from message memos.api.v1.BatchGetUsersResponse
*/
export type BatchGetUsersResponse = Message<"memos.api.v1.BatchGetUsersResponse"> & {
/**
* @generated from field: repeated memos.api.v1.User users = 1;
*/
users: User[];
};
/**
* Describes the message memos.api.v1.BatchGetUsersResponse.
* Use `create(BatchGetUsersResponseSchema)` to create a new message.
*/
export const BatchGetUsersResponseSchema: GenMessage<BatchGetUsersResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 4);
/**
* @generated from message memos.api.v1.GetUserRequest
*/
......@@ -249,7 +283,7 @@ export type GetUserRequest = Message<"memos.api.v1.GetUserRequest"> & {
* Use `create(GetUserRequestSchema)` to create a new message.
*/
export const GetUserRequestSchema: GenMessage<GetUserRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 3);
messageDesc(file_api_v1_user_service, 5);
/**
* @generated from message memos.api.v1.CreateUserRequest
......@@ -292,7 +326,7 @@ export type CreateUserRequest = Message<"memos.api.v1.CreateUserRequest"> & {
* Use `create(CreateUserRequestSchema)` to create a new message.
*/
export const CreateUserRequestSchema: GenMessage<CreateUserRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 4);
messageDesc(file_api_v1_user_service, 6);
/**
* @generated from message memos.api.v1.UpdateUserRequest
......@@ -325,7 +359,7 @@ export type UpdateUserRequest = Message<"memos.api.v1.UpdateUserRequest"> & {
* Use `create(UpdateUserRequestSchema)` to create a new message.
*/
export const UpdateUserRequestSchema: GenMessage<UpdateUserRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 5);
messageDesc(file_api_v1_user_service, 7);
/**
* @generated from message memos.api.v1.DeleteUserRequest
......@@ -352,7 +386,7 @@ export type DeleteUserRequest = Message<"memos.api.v1.DeleteUserRequest"> & {
* Use `create(DeleteUserRequestSchema)` to create a new message.
*/
export const DeleteUserRequestSchema: GenMessage<DeleteUserRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 6);
messageDesc(file_api_v1_user_service, 8);
/**
* User statistics messages
......@@ -409,7 +443,7 @@ export type UserStats = Message<"memos.api.v1.UserStats"> & {
* Use `create(UserStatsSchema)` to create a new message.
*/
export const UserStatsSchema: GenMessage<UserStats> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 7);
messageDesc(file_api_v1_user_service, 9);
/**
* Memo type statistics.
......@@ -443,7 +477,7 @@ export type UserStats_MemoTypeStats = Message<"memos.api.v1.UserStats.MemoTypeSt
* Use `create(UserStats_MemoTypeStatsSchema)` to create a new message.
*/
export const UserStats_MemoTypeStatsSchema: GenMessage<UserStats_MemoTypeStats> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 7, 0);
messageDesc(file_api_v1_user_service, 9, 0);
/**
* @generated from message memos.api.v1.GetUserStatsRequest
......@@ -463,7 +497,7 @@ export type GetUserStatsRequest = Message<"memos.api.v1.GetUserStatsRequest"> &
* Use `create(GetUserStatsRequestSchema)` to create a new message.
*/
export const GetUserStatsRequestSchema: GenMessage<GetUserStatsRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 8);
messageDesc(file_api_v1_user_service, 10);
/**
* This endpoint doesn't take any parameters.
......@@ -478,7 +512,7 @@ export type ListAllUserStatsRequest = Message<"memos.api.v1.ListAllUserStatsRequ
* Use `create(ListAllUserStatsRequestSchema)` to create a new message.
*/
export const ListAllUserStatsRequestSchema: GenMessage<ListAllUserStatsRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 9);
messageDesc(file_api_v1_user_service, 11);
/**
* @generated from message memos.api.v1.ListAllUserStatsResponse
......@@ -497,7 +531,7 @@ export type ListAllUserStatsResponse = Message<"memos.api.v1.ListAllUserStatsRes
* Use `create(ListAllUserStatsResponseSchema)` to create a new message.
*/
export const ListAllUserStatsResponseSchema: GenMessage<ListAllUserStatsResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 10);
messageDesc(file_api_v1_user_service, 12);
/**
* User settings message
......@@ -537,7 +571,7 @@ export type UserSetting = Message<"memos.api.v1.UserSetting"> & {
* Use `create(UserSettingSchema)` to create a new message.
*/
export const UserSettingSchema: GenMessage<UserSetting> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 11);
messageDesc(file_api_v1_user_service, 13);
/**
* General user settings configuration.
......@@ -574,7 +608,7 @@ export type UserSetting_GeneralSetting = Message<"memos.api.v1.UserSetting.Gener
* Use `create(UserSetting_GeneralSettingSchema)` to create a new message.
*/
export const UserSetting_GeneralSettingSchema: GenMessage<UserSetting_GeneralSetting> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 11, 0);
messageDesc(file_api_v1_user_service, 13, 0);
/**
* User webhooks configuration.
......@@ -595,7 +629,7 @@ export type UserSetting_WebhooksSetting = Message<"memos.api.v1.UserSetting.Webh
* Use `create(UserSetting_WebhooksSettingSchema)` to create a new message.
*/
export const UserSetting_WebhooksSettingSchema: GenMessage<UserSetting_WebhooksSetting> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 11, 1);
messageDesc(file_api_v1_user_service, 13, 1);
/**
* Enumeration of user setting keys.
......@@ -627,7 +661,7 @@ export enum UserSetting_Key {
* Describes the enum memos.api.v1.UserSetting.Key.
*/
export const UserSetting_KeySchema: GenEnum<UserSetting_Key> = /*@__PURE__*/
enumDesc(file_api_v1_user_service, 11, 0);
enumDesc(file_api_v1_user_service, 13, 0);
/**
* @generated from message memos.api.v1.GetUserSettingRequest
......@@ -647,7 +681,7 @@ export type GetUserSettingRequest = Message<"memos.api.v1.GetUserSettingRequest"
* Use `create(GetUserSettingRequestSchema)` to create a new message.
*/
export const GetUserSettingRequestSchema: GenMessage<GetUserSettingRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 12);
messageDesc(file_api_v1_user_service, 14);
/**
* @generated from message memos.api.v1.UpdateUserSettingRequest
......@@ -673,7 +707,7 @@ export type UpdateUserSettingRequest = Message<"memos.api.v1.UpdateUserSettingRe
* Use `create(UpdateUserSettingRequestSchema)` to create a new message.
*/
export const UpdateUserSettingRequestSchema: GenMessage<UpdateUserSettingRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 13);
messageDesc(file_api_v1_user_service, 15);
/**
* Request message for ListUserSettings method.
......@@ -713,7 +747,7 @@ export type ListUserSettingsRequest = Message<"memos.api.v1.ListUserSettingsRequ
* Use `create(ListUserSettingsRequestSchema)` to create a new message.
*/
export const ListUserSettingsRequestSchema: GenMessage<ListUserSettingsRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 14);
messageDesc(file_api_v1_user_service, 16);
/**
* Response message for ListUserSettings method.
......@@ -749,7 +783,7 @@ export type ListUserSettingsResponse = Message<"memos.api.v1.ListUserSettingsRes
* Use `create(ListUserSettingsResponseSchema)` to create a new message.
*/
export const ListUserSettingsResponseSchema: GenMessage<ListUserSettingsResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 15);
messageDesc(file_api_v1_user_service, 17);
/**
* PersonalAccessToken represents a long-lived token for API/script access.
......@@ -800,7 +834,7 @@ export type PersonalAccessToken = Message<"memos.api.v1.PersonalAccessToken"> &
* Use `create(PersonalAccessTokenSchema)` to create a new message.
*/
export const PersonalAccessTokenSchema: GenMessage<PersonalAccessToken> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 16);
messageDesc(file_api_v1_user_service, 18);
/**
* @generated from message memos.api.v1.ListPersonalAccessTokensRequest
......@@ -834,7 +868,7 @@ export type ListPersonalAccessTokensRequest = Message<"memos.api.v1.ListPersonal
* Use `create(ListPersonalAccessTokensRequestSchema)` to create a new message.
*/
export const ListPersonalAccessTokensRequestSchema: GenMessage<ListPersonalAccessTokensRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 17);
messageDesc(file_api_v1_user_service, 19);
/**
* @generated from message memos.api.v1.ListPersonalAccessTokensResponse
......@@ -867,7 +901,7 @@ export type ListPersonalAccessTokensResponse = Message<"memos.api.v1.ListPersona
* Use `create(ListPersonalAccessTokensResponseSchema)` to create a new message.
*/
export const ListPersonalAccessTokensResponseSchema: GenMessage<ListPersonalAccessTokensResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 18);
messageDesc(file_api_v1_user_service, 20);
/**
* @generated from message memos.api.v1.CreatePersonalAccessTokenRequest
......@@ -901,7 +935,7 @@ export type CreatePersonalAccessTokenRequest = Message<"memos.api.v1.CreatePerso
* Use `create(CreatePersonalAccessTokenRequestSchema)` to create a new message.
*/
export const CreatePersonalAccessTokenRequestSchema: GenMessage<CreatePersonalAccessTokenRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 19);
messageDesc(file_api_v1_user_service, 21);
/**
* @generated from message memos.api.v1.CreatePersonalAccessTokenResponse
......@@ -928,7 +962,7 @@ export type CreatePersonalAccessTokenResponse = Message<"memos.api.v1.CreatePers
* Use `create(CreatePersonalAccessTokenResponseSchema)` to create a new message.
*/
export const CreatePersonalAccessTokenResponseSchema: GenMessage<CreatePersonalAccessTokenResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 20);
messageDesc(file_api_v1_user_service, 22);
/**
* @generated from message memos.api.v1.DeletePersonalAccessTokenRequest
......@@ -948,7 +982,7 @@ export type DeletePersonalAccessTokenRequest = Message<"memos.api.v1.DeletePerso
* Use `create(DeletePersonalAccessTokenRequestSchema)` to create a new message.
*/
export const DeletePersonalAccessTokenRequestSchema: GenMessage<DeletePersonalAccessTokenRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 21);
messageDesc(file_api_v1_user_service, 23);
/**
* UserWebhook represents a webhook owned by a user.
......@@ -998,7 +1032,7 @@ export type UserWebhook = Message<"memos.api.v1.UserWebhook"> & {
* Use `create(UserWebhookSchema)` to create a new message.
*/
export const UserWebhookSchema: GenMessage<UserWebhook> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 22);
messageDesc(file_api_v1_user_service, 24);
/**
* @generated from message memos.api.v1.ListUserWebhooksRequest
......@@ -1018,7 +1052,7 @@ export type ListUserWebhooksRequest = Message<"memos.api.v1.ListUserWebhooksRequ
* Use `create(ListUserWebhooksRequestSchema)` to create a new message.
*/
export const ListUserWebhooksRequestSchema: GenMessage<ListUserWebhooksRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 23);
messageDesc(file_api_v1_user_service, 25);
/**
* @generated from message memos.api.v1.ListUserWebhooksResponse
......@@ -1037,7 +1071,7 @@ export type ListUserWebhooksResponse = Message<"memos.api.v1.ListUserWebhooksRes
* Use `create(ListUserWebhooksResponseSchema)` to create a new message.
*/
export const ListUserWebhooksResponseSchema: GenMessage<ListUserWebhooksResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 24);
messageDesc(file_api_v1_user_service, 26);
/**
* @generated from message memos.api.v1.CreateUserWebhookRequest
......@@ -1064,7 +1098,7 @@ export type CreateUserWebhookRequest = Message<"memos.api.v1.CreateUserWebhookRe
* Use `create(CreateUserWebhookRequestSchema)` to create a new message.
*/
export const CreateUserWebhookRequestSchema: GenMessage<CreateUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 25);
messageDesc(file_api_v1_user_service, 27);
/**
* @generated from message memos.api.v1.UpdateUserWebhookRequest
......@@ -1090,7 +1124,7 @@ export type UpdateUserWebhookRequest = Message<"memos.api.v1.UpdateUserWebhookRe
* Use `create(UpdateUserWebhookRequestSchema)` to create a new message.
*/
export const UpdateUserWebhookRequestSchema: GenMessage<UpdateUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 26);
messageDesc(file_api_v1_user_service, 28);
/**
* @generated from message memos.api.v1.DeleteUserWebhookRequest
......@@ -1110,7 +1144,7 @@ export type DeleteUserWebhookRequest = Message<"memos.api.v1.DeleteUserWebhookRe
* Use `create(DeleteUserWebhookRequestSchema)` to create a new message.
*/
export const DeleteUserWebhookRequestSchema: GenMessage<DeleteUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 27);
messageDesc(file_api_v1_user_service, 29);
/**
* @generated from message memos.api.v1.UserNotification
......@@ -1132,6 +1166,13 @@ export type UserNotification = Message<"memos.api.v1.UserNotification"> & {
*/
sender: string;
/**
* The sender user details.
*
* @generated from field: memos.api.v1.User sender_user = 8;
*/
senderUser?: User;
/**
* The status of the notification.
*
......@@ -1162,6 +1203,12 @@ export type UserNotification = Message<"memos.api.v1.UserNotification"> & {
*/
value: UserNotification_MemoCommentPayload;
case: "memoComment";
} | {
/**
* @generated from field: memos.api.v1.UserNotification.MemoMentionPayload memo_mention = 7;
*/
value: UserNotification_MemoMentionPayload;
case: "memoMention";
} | { case: undefined; value?: undefined };
};
......@@ -1170,7 +1217,7 @@ export type UserNotification = Message<"memos.api.v1.UserNotification"> & {
* Use `create(UserNotificationSchema)` to create a new message.
*/
export const UserNotificationSchema: GenMessage<UserNotification> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 28);
messageDesc(file_api_v1_user_service, 30);
/**
* @generated from message memos.api.v1.UserNotification.MemoCommentPayload
......@@ -1191,6 +1238,20 @@ export type UserNotification_MemoCommentPayload = Message<"memos.api.v1.UserNoti
* @generated from field: string related_memo = 2;
*/
relatedMemo: string;
/**
* Preview text of the comment memo.
*
* @generated from field: string memo_snippet = 3;
*/
memoSnippet: string;
/**
* Preview text of the related memo.
*
* @generated from field: string related_memo_snippet = 4;
*/
relatedMemoSnippet: string;
};
/**
......@@ -1198,7 +1259,49 @@ export type UserNotification_MemoCommentPayload = Message<"memos.api.v1.UserNoti
* Use `create(UserNotification_MemoCommentPayloadSchema)` to create a new message.
*/
export const UserNotification_MemoCommentPayloadSchema: GenMessage<UserNotification_MemoCommentPayload> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 28, 0);
messageDesc(file_api_v1_user_service, 30, 0);
/**
* @generated from message memos.api.v1.UserNotification.MemoMentionPayload
*/
export type UserNotification_MemoMentionPayload = Message<"memos.api.v1.UserNotification.MemoMentionPayload"> & {
/**
* The memo that contains the mention.
* Format: memos/{memo}
*
* @generated from field: string memo = 1;
*/
memo: string;
/**
* The related parent memo when the mention was created in a comment.
* Format: memos/{memo}
*
* @generated from field: string related_memo = 2;
*/
relatedMemo: string;
/**
* Preview text of the memo that contains the mention.
*
* @generated from field: string memo_snippet = 3;
*/
memoSnippet: string;
/**
* Preview text of the related parent memo.
*
* @generated from field: string related_memo_snippet = 4;
*/
relatedMemoSnippet: string;
};
/**
* Describes the message memos.api.v1.UserNotification.MemoMentionPayload.
* Use `create(UserNotification_MemoMentionPayloadSchema)` to create a new message.
*/
export const UserNotification_MemoMentionPayloadSchema: GenMessage<UserNotification_MemoMentionPayload> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30, 1);
/**
* @generated from enum memos.api.v1.UserNotification.Status
......@@ -1224,7 +1327,7 @@ export enum UserNotification_Status {
* Describes the enum memos.api.v1.UserNotification.Status.
*/
export const UserNotification_StatusSchema: GenEnum<UserNotification_Status> = /*@__PURE__*/
enumDesc(file_api_v1_user_service, 28, 0);
enumDesc(file_api_v1_user_service, 30, 0);
/**
* @generated from enum memos.api.v1.UserNotification.Type
......@@ -1239,13 +1342,18 @@ export enum UserNotification_Type {
* @generated from enum value: MEMO_COMMENT = 1;
*/
MEMO_COMMENT = 1,
/**
* @generated from enum value: MEMO_MENTION = 2;
*/
MEMO_MENTION = 2,
}
/**
* Describes the enum memos.api.v1.UserNotification.Type.
*/
export const UserNotification_TypeSchema: GenEnum<UserNotification_Type> = /*@__PURE__*/
enumDesc(file_api_v1_user_service, 28, 1);
enumDesc(file_api_v1_user_service, 30, 1);
/**
* @generated from message memos.api.v1.ListUserNotificationsRequest
......@@ -1280,7 +1388,7 @@ export type ListUserNotificationsRequest = Message<"memos.api.v1.ListUserNotific
* Use `create(ListUserNotificationsRequestSchema)` to create a new message.
*/
export const ListUserNotificationsRequestSchema: GenMessage<ListUserNotificationsRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 29);
messageDesc(file_api_v1_user_service, 31);
/**
* @generated from message memos.api.v1.ListUserNotificationsResponse
......@@ -1302,7 +1410,7 @@ export type ListUserNotificationsResponse = Message<"memos.api.v1.ListUserNotifi
* Use `create(ListUserNotificationsResponseSchema)` to create a new message.
*/
export const ListUserNotificationsResponseSchema: GenMessage<ListUserNotificationsResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30);
messageDesc(file_api_v1_user_service, 32);
/**
* @generated from message memos.api.v1.UpdateUserNotificationRequest
......@@ -1324,7 +1432,7 @@ export type UpdateUserNotificationRequest = Message<"memos.api.v1.UpdateUserNoti
* Use `create(UpdateUserNotificationRequestSchema)` to create a new message.
*/
export const UpdateUserNotificationRequestSchema: GenMessage<UpdateUserNotificationRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 31);
messageDesc(file_api_v1_user_service, 33);
/**
* @generated from message memos.api.v1.DeleteUserNotificationRequest
......@@ -1343,7 +1451,7 @@ export type DeleteUserNotificationRequest = Message<"memos.api.v1.DeleteUserNoti
* Use `create(DeleteUserNotificationRequestSchema)` to create a new message.
*/
export const DeleteUserNotificationRequestSchema: GenMessage<DeleteUserNotificationRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 32);
messageDesc(file_api_v1_user_service, 34);
/**
* @generated from service memos.api.v1.UserService
......@@ -1359,6 +1467,16 @@ export const UserService: GenService<{
input: typeof ListUsersRequestSchema;
output: typeof ListUsersResponseSchema;
},
/**
* BatchGetUsers returns active users by usernames.
*
* @generated from rpc memos.api.v1.UserService.BatchGetUsers
*/
batchGetUsers: {
methodKind: "unary";
input: typeof BatchGetUsersRequestSchema;
output: typeof BatchGetUsersResponseSchema;
},
/**
* GetUser gets a user by username.
* Format: users/{username} (e.g., users/steven)
......
import type { Root, Text } from "mdast";
import type { Node as UnistNode } from "unist";
import { visit } from "unist-util-visit";
import type { MentionNode, MentionNodeData } from "@/types/markdown";
const MAX_MENTION_LENGTH = 32;
function isMentionChar(char: string): boolean {
return /[A-Za-z0-9-]/.test(char);
}
function isMentionBoundary(char: string): boolean {
if (!char) return true;
return !isMentionChar(char);
}
type Segment = { type: "text"; value: string } | { type: "mention"; value: string };
export function parseMentionsFromText(text: string): Segment[] {
const segments: Segment[] = [];
const chars = [...text];
let i = 0;
while (i < chars.length) {
const prevChar = i > 0 ? chars[i - 1] : "";
if (chars[i] === "@" && isMentionBoundary(prevChar) && i + 1 < chars.length && isMentionChar(chars[i + 1])) {
let j = i + 1;
while (j < chars.length && isMentionChar(chars[j]) && j - i - 1 < MAX_MENTION_LENGTH) {
j++;
}
const username = chars.slice(i + 1, j).join("");
const hasLetterOrNumber = [...username].some((char) => /[A-Za-z0-9]/.test(char));
if (username && hasLetterOrNumber) {
segments.push({ type: "mention", value: username.toLowerCase() });
i = j;
continue;
}
}
let j = i + 1;
while (j < chars.length && chars[j] !== "@") {
j++;
}
segments.push({ type: "text", value: chars.slice(i, j).join("") });
i = j;
}
return segments;
}
export function extractMentionUsernames(text: string): string[] {
const usernames = parseMentionsFromText(text)
.filter((segment): segment is { type: "mention"; value: string } => segment.type === "mention")
.map((segment) => segment.value);
return Array.from(new Set(usernames));
}
function createMentionNode(username: string): MentionNode {
const data: MentionNodeData = {
hName: "span",
hProperties: {
className: "mention",
"data-mention": username,
},
hChildren: [{ type: "text", value: `@${username}` }],
};
return {
type: "mentionNode",
value: username,
data,
} as MentionNode;
}
export const remarkMention = () => {
return (tree: Root) => {
visit(tree, (node, index, parent) => {
if (node.type !== "text" || !parent || index === null) return;
const textNode = node as Text;
const segments = parseMentionsFromText(textNode.value);
if (segments.every((segment) => segment.type === "text")) {
return;
}
const newNodes = segments.map((segment) => {
if (segment.type === "mention") {
return createMentionNode(segment.value);
}
return {
type: "text",
value: segment.value,
} as Text;
});
if (typeof index === "number") {
(parent.children as UnistNode[]).splice(index, 1, ...(newNodes as UnistNode[]));
}
});
};
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment