Skip to content

feat: add appflowy-flutter://new deep link to create notes from clipper#8719

Open
alexrosepizant wants to merge 2 commits into
AppFlowy-IO:mainfrom
alexrosepizant:feat/deeplink-new-note
Open

feat: add appflowy-flutter://new deep link to create notes from clipper#8719
alexrosepizant wants to merge 2 commits into
AppFlowy-IO:mainfrom
alexrosepizant:feat/deeplink-new-note

Conversation

@alexrosepizant
Copy link
Copy Markdown

@alexrosepizant alexrosepizant commented May 13, 2026

Implements the appflowy-flutter://new deep link endpoint that allows external clippers to create documents inside AppFlowy.

Supported query parameters:

  • workspace_id (optional) switch to target workspace first
  • parent_view_id (optional) target space / folder; falls back to current space
  • name (optional) document title; defaults to "New Note"
  • content (optional) URL-encoded Markdown body
  • clipboard (optional flag) read Markdown content from the system clipboard
    (takes precedence over &content when both are present)

Feature Preview

fixes #8718

PR Checklist

  • My code adheres to AppFlowy's Conventions
  • I've listed at least one issue that this PR fixes in the description above.
  • I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
  • All existing tests are passing.

Summary by Sourcery

Add a deep link flow to create new markdown-backed notes from external sources via the appflowy-flutter://new URI.

New Features:

  • Introduce NewNoteDeepLinkHandler to handle appflowy-flutter://new links with optional workspace, parent view, name, and content parameters.
  • Allow pre-filling new notes with markdown content from either a URL-encoded query parameter or the system clipboard via a global note creation notifier.
  • Automatically open newly created document views in the current workspace sidebar when triggered by the new deep link.

Tests:

  • Add unit tests covering NewNoteDeepLinkHandler parsing, clipboard precedence over content, default values, and workspace/parent view handling.

Implements the `appflowy-flutter://new` deep link endpoint that allows
external clippers to create documents inside AppFlowy.

Supported query parameters:
  - workspace_id   (optional) switch to target workspace first
  - parent_view_id (optional) target space / folder; falls back to current space
  - name           (optional) document title; defaults to \"New Note\"
  - content        (optional) URL-encoded Markdown body
  - clipboard      (optional flag) read Markdown content from the system clipboard
                   (takes precedence over &content when both are present)

New files:
  - lib/startup/tasks/deeplink/new_note_deeplink_handler.dart
      DeepLinkHandler that parses the URI, optionally reads the clipboard,
      and signals note creation via createNoteNotifier.
  - lib/workspace/presentation/home/menu/sidebar/workspace/note_creation_notifier.dart
      Global ValueNotifier<CreateNoteParams?> decoupling the handler
      (no BuildContext) from the sidebar (has Bloc context).

Modified files:
  - lib/startup/tasks/appflowy_cloud_task.dart
      Registers NewNoteDeepLinkHandler in the handler registry.
  - lib/workspace/presentation/home/menu/sidebar/sidebar.dart
      _SidebarState listens to createNoteNotifier; handles workspace
      switching with retry logic, converts Markdown to DocumentDataPB,
      calls ViewBackendService.createView and opens the new view.
  - test/unit_test/deeplink/deeplink_test.dart
      8 new unit tests covering canHandle, param mapping, name defaults,
      clipboard integration and priority rules."
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 13, 2026

Reviewer's Guide

Adds a new appflowy-flutter://new deep link handler that queues note-creation parameters via a global notifier, and extends the sidebar to consume those parameters by switching workspaces if needed, creating a new document (optionally from Markdown or clipboard content), and opening it; also wires the handler into startup and adds unit tests.

Sequence diagram for the new appflowy-flutter-new deep link note creation

sequenceDiagram
  actor ExternalClipper
  participant DeepLinkRouter
  participant NewNoteDeepLinkHandler
  participant createNoteNotifier
  participant Sidebar
  participant UserWorkspaceBloc
  participant ViewBackendService
  participant TabsBloc

  ExternalClipper->>DeepLinkRouter: open(appflowy-flutter://new?...)
  DeepLinkRouter->>NewNoteDeepLinkHandler: handle(uri)
  NewNoteDeepLinkHandler->>NewNoteDeepLinkHandler: parse queryParameters
  alt clipboard flag present
    NewNoteDeepLinkHandler->>Clipboard: getData(Clipboard.kTextPlain)
    Clipboard-->>NewNoteDeepLinkHandler: text
  else content param only
    NewNoteDeepLinkHandler->>NewNoteDeepLinkHandler: read content param
  end
  NewNoteDeepLinkHandler->>createNoteNotifier: set(CreateNoteParams)

  createNoteNotifier-->>Sidebar: listener _handleCreateNoteDeepLink()
  Sidebar->>UserWorkspaceBloc: read currentWorkspace
  alt workspaceId specified and not current
    Sidebar->>UserWorkspaceBloc: fetchWorkspaces/openWorkspace
    Sidebar->>Sidebar: schedule _handleCreateNoteDeepLink (Future.delayed)
  else workspace ready
    Sidebar->>Sidebar: _createNoteFromDeepLink(params)
    Sidebar->>createNoteNotifier: set(null)
    Sidebar->>ViewBackendService: createView(ViewLayoutPB.Document,...)
    ViewBackendService-->>Sidebar: view or error
    alt view created
      Sidebar->>TabsBloc: openPlugin(view)
    else error
      Sidebar->>Sidebar: log error
    end
  end
Loading

File-Level Changes

Change Details Files
Introduce NewNoteDeepLinkHandler to parse appflowy-flutter://new URIs and populate a global note-creation notifier, including optional clipboard content handling and sensible defaults.
  • Create NewNoteDeepLinkHandler that recognizes host 'new' and extracts workspace_id, parent_view_id, name, content, and clipboard parameters.
  • Implement logic where the clipboard flag, if present, reads text from Clipboard and overrides any content query parameter, logging a warning when clipboard is empty.
  • Set createNoteNotifier with a CreateNoteParams instance and log the queued creation, returning a successful FlowyResult.
frontend/appflowy_flutter/lib/startup/tasks/deeplink/new_note_deeplink_handler.dart
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/note_creation_notifier.dart
Extend the sidebar to listen for deep-link-driven note creation requests, handle workspace switching with retries, and create/open a new document with optional Markdown initialization.
  • Register a listener on createNoteNotifier in _SidebarState.initState and remove it in dispose, and track retry count with a max retry limit for workspace switching.
  • Implement _handleCreateNoteDeepLink to ensure the target workspace is current (fetching or opening it as needed, with delayed retries) before creating the note, logging and aborting after exceeding retry attempts.
  • Implement _createNoteFromDeepLink to resolve the parent view (falling back to current space), convert Markdown content to DocumentDataPB bytes when provided, call ViewBackendService.createView with initialDataBytes, and open the resulting view in TabsBloc or log errors.
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/note_creation_notifier.dart
Wire the new deep link handler into the existing deep link infrastructure for both tests and AppFlowy Cloud startup, and add unit coverage for query parameter and clipboard behavior.
  • Register NewNoteDeepLinkHandler with DeepLinkHandlerRegistry in deeplink_test and AppFlowyCloudDeepLink so appflowy-flutter://new links are recognized in both environments.
  • Add unit tests covering canHandle behavior, parameter mapping to CreateNoteParams, defaulting of name and IDs, clipboard precedence over content, behavior when clipboard is empty, and workspace/parentViewId nullability.
  • Initialize Flutter test bindings to support Clipboard and reset createNoteNotifier between tests.
frontend/appflowy_flutter/test/unit_test/deeplink/deeplink_test.dart
frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#8718 Implement an appflowy-flutter://new deep link handler that supports workspace_id, parent_view_id, name (defaulting to "New Note"), content (URL-encoded Markdown), and a clipboard flag that takes precedence over content when both are present.
#8718 Wire the new deep link into the existing deep link infrastructure so that invoking appflowy-flutter://new actually creates a new document in the appropriate workspace and parent view, converting the provided Markdown into a document and opening it in the UI.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Using a global ValueNotifier<CreateNoteParams?> to bridge between the deep-link handler and the sidebar introduces a hidden cross-layer dependency; consider passing this via an existing bloc/event or a dedicated service so that note creation doesn’t rely on a global mutable singleton.
  • The retry mechanism in _handleCreateNoteDeepLink uses a shared _noteCreationRetryCount and recursively schedules itself; it might be safer to tie retries to a specific request (or cancel them when a new deep link arrives) to avoid overlapping retries and unexpected behavior if multiple deep links are processed close together.
  • In _createNoteFromDeepLink, the markdown-to-document conversion is assumed to succeed; wrapping customMarkdownToDocument and DocumentDataPBFromTo.fromDocument in a try/catch with a fallback to creating an empty note would prevent a malformed content payload from breaking note creation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using a global `ValueNotifier<CreateNoteParams?>` to bridge between the deep-link handler and the sidebar introduces a hidden cross-layer dependency; consider passing this via an existing bloc/event or a dedicated service so that note creation doesn’t rely on a global mutable singleton.
- The retry mechanism in `_handleCreateNoteDeepLink` uses a shared `_noteCreationRetryCount` and recursively schedules itself; it might be safer to tie retries to a specific request (or cancel them when a new deep link arrives) to avoid overlapping retries and unexpected behavior if multiple deep links are processed close together.
- In `_createNoteFromDeepLink`, the markdown-to-document conversion is assumed to succeed; wrapping `customMarkdownToDocument` and `DocumentDataPBFromTo.fromDocument` in a try/catch with a fallback to creating an empty note would prevent a malformed content payload from breaking note creation.

## Individual Comments

### Comment 1
<location path="frontend/appflowy_flutter/lib/startup/tasks/deeplink/new_note_deeplink_handler.dart" line_range="45-54" />
<code_context>
+    String? content;
+
+    // `clipboard` flag takes precedence over an explicit `content` value.
+    if (uri.queryParameters.containsKey(_clipboardKey)) {
+      final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
+      content = clipboardData?.text;
+      if (content == null || content.isEmpty) {
+        Log.warn('NewNoteDeepLink: clipboard was empty');
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider hardening clipboard reads against platform errors.

Since this runs on the deep-link path, an exception from `Clipboard.getData` (e.g., from a misbehaving platform plugin) would crash the app on startup. Please wrap the clipboard read in a try/catch, log a warning, and fall back to the `content` parameter so failures don’t break app launch.

```suggestion
    // `clipboard` flag takes precedence over an explicit `content` value.
    if (uri.queryParameters.containsKey(_clipboardKey)) {
      try {
        final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
        content = clipboardData?.text;
        if (content == null || content.isEmpty) {
          Log.warn('NewNoteDeepLink: clipboard was empty');
          // Fall back to the `content` query parameter when clipboard is empty.
          content = uri.queryParameters[_contentKey];
        }
      } catch (e, stackTrace) {
        // Harden against platform/plugin errors when reading the clipboard.
        Log.warn('NewNoteDeepLink: failed to read clipboard: $e');
        // Fall back to the `content` query parameter if clipboard access fails.
        content = uri.queryParameters[_contentKey];
      }
    } else {
      content = uri.queryParameters[_contentKey];
    }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Three issues fixed:

1. Replace global ValueNotifier with a getIt-registered CreateNoteService
   The raw `createNoteNotifier` global is replaced by `CreateNoteService
   extends ChangeNotifier`, registered as a lazySingleton in
   DependencyResolver._resolveUserDeps. This makes the cross-layer
   dependency explicit and injectable for tests.

2. Replace polling retry with event-driven workspace-switch
   The shared `_noteCreationRetryCount` counter and recursive
   `Future.delayed` loop are removed. _SidebarState now subscribes to
   `UserWorkspaceBloc.stream`; when the workspace changes to the
   requested one the note is created automatically. A `_pendingWorkspaceSwitchId`
   guard prevents duplicate switch events from being dispatched while
   the switch is still in progress.

3. Wrap Markdown conversion in try/catch
   `customMarkdownToDocument` and `DocumentDataPBFromTo.fromDocument`
   are now wrapped in a try/catch block; a malformed `content` payload
   logs a warning and falls back to creating an empty note instead of
   crashing note creation.

Handler testability: NewNoteDeepLinkHandler now accepts an optional
`CreateNoteService` parameter for DI; tests inject a fresh instance
directly (no getIt setup required)."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Deep link endpoint for external clippers

1 participant