diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
index 30bcc0df..5bed7d5c 100644
--- a/flutter/android/app/src/main/AndroidManifest.xml
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
path;
@@ -206,9 +211,62 @@ LUIFlutterExtensionRegistry logseqChatExtensionRegistry({
],
builder: (context) => _OverflowMenu(context: context),
),
+ )
+ ..register(
+ LUIFlutterExtension(
+ identifier: 'composer-asset',
+ fingerprint: _composerAssetFingerprint,
+ properties: [
+ _requiredStringProperty('title'),
+ _requiredStringProperty('local-path'),
+ ],
+ builder: (context) => _ComposerAssetPreview(context: context),
+ ),
);
}
+final class _ComposerAssetPreview extends StatelessWidget {
+ const _ComposerAssetPreview({required this.context});
+
+ final LUIFlutterExtensionContext context;
+
+ @override
+ Widget build(BuildContext buildContext) {
+ final title = context.property('title') as String? ?? '';
+ final localPath = context.property('local-path') as String? ?? '';
+ final Widget content =
+ localPath.isNotEmpty && isAndroidImageAsset('', localPath)
+ ? Image.file(
+ File(localPath),
+ fit: BoxFit.cover,
+ errorBuilder: (_, _, _) => _fallback(buildContext, title),
+ )
+ : _fallback(buildContext, title);
+ return Semantics(
+ label: title,
+ image: true,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: SizedBox.square(dimension: 128, child: content),
+ ),
+ );
+ }
+
+ Widget _fallback(BuildContext buildContext, String title) => ColoredBox(
+ color: Theme.of(buildContext).colorScheme.surfaceContainerHigh,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 4,
+ children: [
+ const Icon(Icons.attach_file_rounded),
+ Text(title, maxLines: 2, overflow: TextOverflow.ellipsis),
+ ],
+ ),
+ ),
+ );
+}
+
LUIExtensionEventField _requiredStringField(String name) =>
LUIExtensionEventField(
name: name,
@@ -380,6 +438,7 @@ final class _SearchPresentationState extends State<_SearchPresentation>
controller: _controller,
focusNode: _focusNode,
autoFocus: query.isEmpty,
+ textInputAction: TextInputAction.search,
hintText: 'Search pages and blocks',
leading: IconButton(
tooltip: depth > 0 ? 'Back' : 'Close search',
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
index 702ba0d0..ed873afb 100644
--- a/flutter/lib/main.dart
+++ b/flutter/lib/main.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:lui_flutter_backend/lui_flutter_backend.dart';
import 'android_platform_effects.dart';
@@ -32,6 +33,20 @@ void _traceStartup(String message) {
void main() {
_traceStartup('main entered');
WidgetsFlutterBinding.ensureInitialized();
+ unawaited(
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge).then((_) {
+ SystemChrome.setSystemUIOverlayStyle(
+ const SystemUiOverlayStyle(
+ statusBarColor: Colors.transparent,
+ systemNavigationBarColor: Colors.transparent,
+ systemNavigationBarDividerColor: Colors.transparent,
+ systemNavigationBarContrastEnforced: false,
+ statusBarBrightness: Brightness.dark,
+ statusBarIconBrightness: Brightness.dark,
+ ),
+ );
+ }),
+ );
_traceStartup('Flutter binding initialized');
runApp(const LogseqChatFlutterApp());
_traceStartup('runApp returned');
@@ -47,6 +62,7 @@ class LogseqChatFlutterApp extends StatefulWidget {
class _LogseqChatFlutterAppState extends State
with WidgetsBindingObserver {
final _navigatorKey = GlobalKey();
+ final _messengerKey = GlobalKey();
late final LUIFlutterBackend _backend;
late final FlutterPatchApplier _patchApplier;
LogseqChatNativeBridge? _bridge;
@@ -602,6 +618,14 @@ class _LogseqChatFlutterAppState extends State
void _reportEffectError(Object error, StackTrace stack) {
_logRuntime(level: 'error', source: 'ui', message: error.toString());
+ _messengerKey.currentState
+ ?..hideCurrentSnackBar()
+ ..showSnackBar(
+ SnackBar(
+ behavior: SnackBarBehavior.floating,
+ content: Text('Something went wrong: $error'),
+ ),
+ );
FlutterError.reportError(
FlutterErrorDetails(
exception: error,
@@ -654,6 +678,7 @@ class _LogseqChatFlutterAppState extends State
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
+ scaffoldMessengerKey: _messengerKey,
debugShowCheckedModeBanner: false,
title: 'Logseq Chat',
theme: LogseqChatTheme.light(),
@@ -666,9 +691,36 @@ class _LogseqChatFlutterAppState extends State
Widget _content() {
if (_startupError != null) {
return SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: Text('Could not start Logseq Chat: $_startupError'),
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 16,
+ children: [
+ Icon(
+ Icons.error_outline_rounded,
+ size: 48,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ Text(
+ 'Could not start Logseq Chat',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ Text(
+ '$_startupError',
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ FilledButton(
+ onPressed: () {
+ setState(() => _startupError = null);
+ unawaited(_start());
+ },
+ child: const Text('Retry'),
+ ),
+ ],
+ ),
+ ),
),
);
}
diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock
index 39bb6bff..f68c449e 100644
--- a/flutter/pubspec.lock
+++ b/flutter/pubspec.lock
@@ -159,8 +159,8 @@ packages:
dependency: "direct main"
description:
path: "platform/flutter"
- ref: b65da35
- resolved-ref: b65da35dc226b744174b8693151f3262fe5d94ec
+ ref: db506b502fce3350839a194314a9542729e9f819
+ resolved-ref: db506b502fce3350839a194314a9542729e9f819
url: "ssh://git@github.com/logseq/lui.git"
source: git
version: "0.1.0"
diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml
index a62278eb..7e28ac2b 100644
--- a/flutter/pubspec.yaml
+++ b/flutter/pubspec.yaml
@@ -15,7 +15,7 @@ dependencies:
lui_flutter_backend:
git:
url: ssh://git@github.com/logseq/lui.git
- ref: b65da35
+ ref: db506b502fce3350839a194314a9542729e9f819
path: platform/flutter
webview_flutter: ^4.14.1
diff --git a/shared/src/logseq_chat/model.ml b/shared/src/logseq_chat/model.ml
index b0b03747..4c32d1e0 100644
--- a/shared/src/logseq_chat/model.ml
+++ b/shared/src/logseq_chat/model.ml
@@ -1923,7 +1923,11 @@ let rec update (current : chat_model) action =
app_navigation_previews =
remove_node_projection updated.app_navigation_previews uuid;
}
- | BackAppNavigation count -> back_app_navigation current count
+ | BackAppNavigation count ->
+ if current.outliner_selected_block_ids <> [] then
+ let id = current.next_effect_id in
+ enqueue_effect current (OutlinerToolbarEffect (id, "unselect"))
+ else back_app_navigation current count
| RequestSearchNode uuid ->
let path = current.search_navigation_path in
let requested = request_route path (NodeRoute uuid) in
diff --git a/shared/src/logseq_chat/view.ml b/shared/src/logseq_chat/view.ml
index 14e28979..f515b05e 100644
--- a/shared/src/logseq_chat/view.ml
+++ b/shared/src/logseq_chat/view.ml
@@ -4,7 +4,8 @@ let chat_view = View_screens.chat_view
let composer_asset_schema () =
Lui_extension.component "composer-asset"
- [ Lui_protocol.profile Lui_protocol.IOS Lui_protocol.SwiftUIHost ]
+ [ Lui_protocol.profile Lui_protocol.IOS Lui_protocol.SwiftUIHost;
+ Lui_protocol.profile Lui_protocol.AndroidOS Lui_protocol.FlutterHost ]
false []
[
Lui_extension.property "title" Lui_extension.StringScalar true None;
diff --git a/shared/src/logseq_chat/view_base.ml b/shared/src/logseq_chat/view_base.ml
index 08ad25bc..5d993375 100644
--- a/shared/src/logseq_chat/view_base.ml
+++ b/shared/src/logseq_chat/view_base.ml
@@ -260,6 +260,9 @@ let model_graph_menu_open_ (current : Model.chat_model) =
let model_task_status_picker_open_ (current : Model.chat_model) =
current.task_status_picker_open
+let model_attachment_picker_open_ (current : Model.chat_model) =
+ current.attachment_picker_open
+
let model_new_graph_name (current : Model.chat_model) = current.new_graph_name
let model_new_graph_encrypted_ (current : Model.chat_model) =
diff --git a/shared/src/logseq_chat/view_base.mli b/shared/src/logseq_chat/view_base.mli
index 741026ad..0dc1de8d 100644
--- a/shared/src/logseq_chat/view_base.mli
+++ b/shared/src/logseq_chat/view_base.mli
@@ -62,6 +62,7 @@ val model_community_links :
Model.chat_model -> Model.settings_community_link list
val model_graph_menu_open_ : Model.chat_model -> bool
val model_task_status_picker_open_ : Model.chat_model -> bool
+val model_attachment_picker_open_ : Model.chat_model -> bool
val model_new_graph_name : Model.chat_model -> string
val model_new_graph_encrypted_ : Model.chat_model -> bool
val model_graph_password : Model.chat_model -> string
diff --git a/shared/src/logseq_chat/view_composer.ml b/shared/src/logseq_chat/view_composer.ml
index 05125e22..bd176836 100644
--- a/shared/src/logseq_chat/view_composer.ml
+++ b/shared/src/logseq_chat/view_composer.ml
@@ -30,6 +30,36 @@ let attachment_menu send : t =
[];
]
+let attachment_picker_menu send : t =
+ dropdown_menu ~anchor:`above ~anchor_alignment:`start ~min_width:200
+ ~on_dismiss:(press send Model.CloseAttachmentPicker)
+ [
+ menu_item ~icon:(`app "toolbar-attachment")
+ ~accessibility_identifier:"button.attachment.files"
+ ~text:"File"
+ ~on_press:(fun _ ->
+ ignore (send (Model.ChooseAttachment "files")))
+ [];
+ menu_item ~icon:(`app "toolbar-camera")
+ ~accessibility_identifier:"button.attachment.camera"
+ ~text:"Camera"
+ ~on_press:(fun _ ->
+ ignore (send (Model.ChooseAttachment "camera")))
+ [];
+ menu_item ~icon:(`app "composer-photo")
+ ~accessibility_identifier:"button.attachment.photos"
+ ~text:"Photo"
+ ~on_press:(fun _ ->
+ ignore (send (Model.ChooseAttachment "photos")))
+ [];
+ menu_item ~icon:(`app "toolbar-audio")
+ ~accessibility_identifier:"button.attachment.audio"
+ ~text:"Audio recording"
+ ~on_press:(fun _ ->
+ ignore (send (Model.ChooseAttachment "audio")))
+ [];
+ ]
+
let composer_attachment_button (context : Lui_ui.ui_context) send : t =
if Lui_ui.platform context = AndroidOS then
button ~icon:(`app "add") ~variant:`ghost ~label:"Add attachment"
@@ -230,7 +260,18 @@ let composer_view (context : Lui_ui.ui_context) model_source send : t =
row ~gap:8 ~height:44 ~cross:`center
~accessibility_identifier:"row.composer.controls"
[
- composer_attachment_button context send;
+ if Lui_ui.host context = FlutterHost then
+ stack
+ [
+ composer_attachment_button context send;
+ if_
+ ~test:
+ (Signal.map
+ View_base.model_attachment_picker_open_
+ model_source)
+ (attachment_picker_menu send);
+ ]
+ else composer_attachment_button context send;
stack
[
composer_task_status_button context send;
diff --git a/shared/src/logseq_chat/view_graphs.ml b/shared/src/logseq_chat/view_graphs.ml
index 31a4367d..70338d15 100644
--- a/shared/src/logseq_chat/view_graphs.ml
+++ b/shared/src/logseq_chat/view_graphs.ml
@@ -378,11 +378,61 @@ let graph_picker_error_banner model_source : t =
];
]
-let graph_password_sheet model_source send : t =
- sheet ~text:"Unlock encrypted graphs" ~style_class:"navigation-form"
- ~accessibility_identifier:"sheet.graph-unlock"
- ~on_dismiss:(press send Model.CancelGraphUnlock)
- [
+let graph_password_sheet (context : Lui_ui.ui_context) model_source send
+ : t =
+ if Lui_ui.host context = FlutterHost then
+ sheet ~text:"Unlock encrypted graphs" ~height:360
+ ~accessibility_identifier:"sheet.graph-unlock"
+ ~on_dismiss:(press send Model.CancelGraphUnlock)
+ [
+ column ~gap:20 ~cross:`stretch ~grow:1.0
+ ~accessibility_identifier:"form.graph-unlock"
+ [
+ secure_field
+ ~text_signal:
+ (Signal.map View_base.model_graph_password model_source)
+ ~placeholder:"E2EE password" ~label:"E2EE password"
+ ~accessibility_identifier:"field.graph-password"
+ ~on_input:
+ (on_input send (fun text ->
+ Model.ChangeGraphPassword text))
+ [];
+ text ~style_class:"footnote" ~foreground:"muted-foreground"
+ ~value:"Enter your E2EE password to unlock this graph." [];
+ if_
+ ~test:
+ (Signal.map View_base.graph_unlock_error_present_
+ model_source)
+ (text
+ ~value_signal:
+ (Signal.map View_base.graph_unlock_error_message
+ model_source)
+ ~style_class:"footnote" ~foreground:"destructive"
+ ~accessibility_identifier:"text.graph-unlock-error" []);
+ spacer ~grow:1.0 [];
+ toolbar ~orientation:`horizontal ~label:"Graph unlock actions"
+ ~accessibility_identifier:"toolbar.graph-unlock"
+ [
+ spacer ~grow:1.0 [];
+ button ~variant:`ghost
+ ~accessibility_identifier:"button.graph-unlock.cancel"
+ ~on_press:(press send Model.CancelGraphUnlock)
+ ~text:"Cancel" [];
+ button ~variant:`primary
+ ~accessibility_identifier:"button.graph-unlock"
+ ~disabled_signal:
+ (Signal.map View_base.graph_unlock_disabled_
+ model_source)
+ ~on_press:(press send Model.SubmitGraphPassword)
+ ~text:"Unlock" [];
+ ];
+ ];
+ ]
+ else
+ sheet ~text:"Unlock encrypted graphs" ~style_class:"navigation-form"
+ ~accessibility_identifier:"sheet.graph-unlock"
+ ~on_dismiss:(press send Model.CancelGraphUnlock)
+ [
column ~style_class:"form"
~accessibility_identifier:"form.graph-unlock"
[
diff --git a/shared/src/logseq_chat/view_graphs.mli b/shared/src/logseq_chat/view_graphs.mli
index 2ce03d7b..cfbf1885 100644
--- a/shared/src/logseq_chat/view_graphs.mli
+++ b/shared/src/logseq_chat/view_graphs.mli
@@ -21,6 +21,7 @@ val graph_picker_overflow_menu :
val graph_picker_error_banner :
Model.chat_model Signal.signal -> Lui_elements.t
val graph_password_sheet :
+ Lui_ui.ui_context ->
Model.chat_model Signal.signal ->
(Model.chat_action -> bool) -> Lui_elements.t
val graphs_screen :
diff --git a/shared/src/logseq_chat/view_screens.ml b/shared/src/logseq_chat/view_screens.ml
index 744ef106..2c0ec3e7 100644
--- a/shared/src/logseq_chat/view_screens.ml
+++ b/shared/src/logseq_chat/view_screens.ml
@@ -791,14 +791,14 @@ let application_main_content (context : Lui_ui.ui_context) model_source
if_
~test:(Signal.map View_base.model_graph_password_open_
model_source)
- (View_graphs.graph_password_sheet model_source send);
+ (View_graphs.graph_password_sheet context model_source send);
if_
~test:(Signal.map View_base.page_deletion_pending_ model_source)
- (View_settings.page_delete_dialog send);
+ (View_settings.page_delete_dialog context send);
if_
~test:(Signal.map View_base.model_sync_details_open_
model_source)
- (View_settings.sync_status_sheet model_source send);
+ (View_settings.sync_status_sheet context model_source send);
]
else
[
@@ -829,14 +829,14 @@ let application_main_content (context : Lui_ui.ui_context) model_source
if_
~test:(Signal.map View_base.model_graph_password_open_
model_source)
- (View_graphs.graph_password_sheet model_source send);
+ (View_graphs.graph_password_sheet context model_source send);
if_
~test:(Signal.map View_base.page_deletion_pending_ model_source)
- (View_settings.page_delete_dialog send);
+ (View_settings.page_delete_dialog context send);
if_
~test:(Signal.map View_base.model_sync_details_open_
model_source)
- (View_settings.sync_status_sheet model_source send);
+ (View_settings.sync_status_sheet context model_source send);
]
in
let stack_grow = Lui_ui.platform context = AndroidOS in
diff --git a/shared/src/logseq_chat/view_settings.ml b/shared/src/logseq_chat/view_settings.ml
index 8da46873..036764d0 100644
--- a/shared/src/logseq_chat/view_settings.ml
+++ b/shared/src/logseq_chat/view_settings.ml
@@ -56,7 +56,25 @@ let settings_language_control (context : Lui_ui.ui_context) model_source
~key:View_base.settings_language_choice_identifier
~cmp:compare
~mount:(fun choice_source ->
- settings_language_choice_menu_item choice_source send);
+ let choice = Signal.sample choice_source in
+ menu_item
+ ~text_signal:
+ (Signal.map
+ View_base.settings_language_choice_title
+ choice_source)
+ ~selected_signal:
+ (Signal.map
+ (fun (model : Model.chat_model) ->
+ Model.settings_language_choice_selected_
+ model choice)
+ model_source)
+ ~accessibility_identifier:
+ (View_base.settings_language_choice_identifier
+ choice)
+ ~on_press:(fun _ ->
+ ignore
+ (send (Model.ChooseSettingsLanguage choice.id)))
+ []);
]);
]
else
@@ -74,6 +92,18 @@ let settings_language_control (context : Lui_ui.ui_context) model_source
let settings_appearance_control (context : Lui_ui.ui_context) model_source
send : t =
if Lui_ui.host context = FlutterHost then
+ let appearance_item id title : t =
+ menu_item ~text:title
+ ~selected_signal:
+ (Signal.map
+ (fun (model : Model.chat_model) -> model.appearance = id)
+ model_source)
+ ~accessibility_identifier:
+ ("picker.settings.appearance.option." ^ id)
+ ~on_press:(fun _ ->
+ ignore (send (Model.ChangeAppearance id)))
+ []
+ in
stack
~accessibility_identifier:"layout.settings.appearance-control"
[
@@ -92,18 +122,9 @@ let settings_appearance_control (context : Lui_ui.ui_context) model_source
~min_width:160
~on_dismiss:(press send Model.CloseSettingsAppearanceMenu)
[
- menu_item ~text:"System"
- ~on_press:(fun _ ->
- ignore (send (Model.ChangeAppearance "system")))
- [];
- menu_item ~text:"Light"
- ~on_press:(fun _ ->
- ignore (send (Model.ChangeAppearance "light")))
- [];
- menu_item ~text:"Dark"
- ~on_press:(fun _ ->
- ignore (send (Model.ChangeAppearance "dark")))
- [];
+ appearance_item "system" "System";
+ appearance_item "light" "Light";
+ appearance_item "dark" "Dark";
]);
]
else
@@ -923,24 +944,54 @@ let settings_sheet (context : Lui_ui.ui_context) model_source send : t =
]
else settings_main_sheet context model_source send
-let page_delete_dialog send : t =
- dialog ~text:"Delete page?" ~style_class:"alert"
- ~accessibility_identifier:"dialog.page-delete"
- ~on_dismiss:(press send Model.CancelDeleteActivePage)
- [
- column
- [
- text ~value:"The page will be moved to Recycle." [];
- button ~accessibility_identifier:"button.page-delete.cancel"
- ~on_press:(press send Model.CancelDeleteActivePage)
- ~text:"Cancel" [];
- button ~accessibility_identifier:"button.page-delete.confirm"
- ~on_press:(press send Model.ConfirmDeleteActivePage)
- ~text:"Delete" [];
- ];
- ]
+let page_delete_dialog (context : Lui_ui.ui_context) send : t =
+ if Lui_ui.host context = FlutterHost then
+ dialog ~text:"Delete page?" ~height:280
+ ~accessibility_identifier:"dialog.page-delete"
+ ~on_dismiss:(press send Model.CancelDeleteActivePage)
+ [
+ column ~gap:16 ~cross:`stretch
+ [
+ text ~value:"The page will be moved to Recycle." ~grow:1.0 [];
+ spacer ~grow:1.0 [];
+ row ~main:`end_
+ [
+ toolbar ~orientation:`horizontal ~toolbar_gap:12
+ ~label:"Page deletion actions"
+ ~accessibility_identifier:"toolbar.page-delete"
+ [
+ button ~variant:`ghost
+ ~accessibility_identifier:
+ "button.page-delete.cancel"
+ ~on_press:(press send Model.CancelDeleteActivePage)
+ ~text:"Cancel" [];
+ button ~variant:`destructive
+ ~accessibility_identifier:
+ "button.page-delete.confirm"
+ ~on_press:(press send Model.ConfirmDeleteActivePage)
+ ~text:"Delete" [];
+ ];
+ ];
+ ];
+ ]
+ else
+ dialog ~text:"Delete page?" ~style_class:"alert"
+ ~accessibility_identifier:"dialog.page-delete"
+ ~on_dismiss:(press send Model.CancelDeleteActivePage)
+ [
+ column
+ [
+ text ~value:"The page will be moved to Recycle." [];
+ button ~accessibility_identifier:"button.page-delete.cancel"
+ ~on_press:(press send Model.CancelDeleteActivePage)
+ ~text:"Cancel" [];
+ button ~accessibility_identifier:"button.page-delete.confirm"
+ ~on_press:(press send Model.ConfirmDeleteActivePage)
+ ~text:"Delete" [];
+ ];
+ ]
-let sync_status_sheet model_source send : t =
+let sync_status_sheet (context : Lui_ui.ui_context) model_source send : t =
let sync_row identifier label value_signal : t =
list_item ~accessibility_identifier:identifier
[
@@ -952,15 +1003,83 @@ let sync_status_sheet model_source send : t =
];
]
in
- sheet ~text:"Sync status" ~style_class:"navigation-form"
- ~accessibility_identifier:"sheet.sync-status"
- ~on_dismiss:(press send Model.CloseSyncDetails)
- [
- column ~style_class:"form"
- ~accessibility_identifier:"form.sync-status"
- [
- sync_row "row.sync.status" "Status"
- (Signal.map View_base.sync_label model_source);
+ if Lui_ui.host context = FlutterHost then
+ sheet ~text:"Sync status" ~height:420
+ ~accessibility_identifier:"sheet.sync-status"
+ ~on_dismiss:(press send Model.CloseSyncDetails)
+ [
+ column ~gap:8 ~cross:`stretch ~grow:1.0
+ ~accessibility_identifier:"form.sync-status"
+ [
+ sync_row "row.sync.status" "Status"
+ (Signal.map View_base.sync_label model_source);
+ sync_row "row.sync.graph" "Graph"
+ (Signal.map View_base.graph_label model_source);
+ sync_row "row.sync.connection" "Connection"
+ (Signal.map View_base.sync_connection_label model_source);
+ list_item ~accessibility_identifier:"row.sync.pending"
+ [
+ row ~grow:1.0 ~cross:`center ~main:`space_between
+ [
+ text ~value:"Local changes" [];
+ text
+ ~value_signal:
+ (Signal.map View_base.sync_pending_label
+ model_source)
+ ~foreground:"secondary" ~text_alignment:`end_
+ ~accessibility_identifier:"sync.pending" [];
+ ];
+ ];
+ list_item ~accessibility_identifier:"row.sync.cursor"
+ [
+ row ~grow:1.0 ~cross:`center ~main:`space_between
+ [
+ text ~value:"Server cursor" [];
+ text
+ ~value_signal:
+ (Signal.map View_base.sync_cursor_label
+ model_source)
+ ~foreground:"secondary" ~text_alignment:`end_
+ ~accessibility_identifier:"sync.cursor" [];
+ ];
+ ];
+ if_
+ ~test:(Signal.map View_base.sync_error_present_ model_source)
+ (text ~style_class:"footnote" ~foreground:"muted-foreground"
+ ~value:"Last error" []);
+ if_
+ ~test:(Signal.map View_base.sync_error_present_ model_source)
+ (text
+ ~value_signal:
+ (Signal.map View_base.sync_error_message model_source)
+ ~foreground:"destructive"
+ ~accessibility_identifier:"sync.error" []);
+ spacer ~grow:1.0 [];
+ button ~variant:`primary ~grow:1.0
+ ~accessibility_identifier:"button.sync-now"
+ ~on_press:(press send Model.SyncNow)
+ ~text:"Sync now" [];
+ toolbar ~orientation:`horizontal ~label:"Sync status actions"
+ ~accessibility_identifier:"toolbar.sync.actions"
+ [
+ spacer ~grow:1.0 [];
+ button ~variant:`ghost
+ ~accessibility_identifier:"button.sync.done"
+ ~on_press:(press send Model.CloseSyncDetails)
+ ~text:"Done" [];
+ ];
+ ];
+ ]
+ else
+ sheet ~text:"Sync status" ~style_class:"navigation-form"
+ ~accessibility_identifier:"sheet.sync-status"
+ ~on_dismiss:(press send Model.CloseSyncDetails)
+ [
+ column ~style_class:"form"
+ ~accessibility_identifier:"form.sync-status"
+ [
+ sync_row "row.sync.status" "Status"
+ (Signal.map View_base.sync_label model_source);
sync_row "row.sync.graph" "Graph"
(Signal.map View_base.graph_label model_source);
sync_row "row.sync.connection" "Connection"
diff --git a/shared/src/logseq_chat/view_settings.mli b/shared/src/logseq_chat/view_settings.mli
index 21cbef85..ded04c22 100644
--- a/shared/src/logseq_chat/view_settings.mli
+++ b/shared/src/logseq_chat/view_settings.mli
@@ -76,7 +76,9 @@ val settings_sheet :
Lui_ui.ui_context ->
Model.chat_model Signal.signal ->
(Model.chat_action -> bool) -> Lui_elements.t
-val page_delete_dialog : (Model.chat_action -> bool) -> Lui_elements.t
+val page_delete_dialog :
+ Lui_ui.ui_context -> (Model.chat_action -> bool) -> Lui_elements.t
val sync_status_sheet :
+ Lui_ui.ui_context ->
Model.chat_model Signal.signal ->
(Model.chat_action -> bool) -> Lui_elements.t
diff --git a/shared/test/logseq_chat/app_test.ml b/shared/test/logseq_chat/app_test.ml
index 67a2976b..509df28e 100644
--- a/shared/test/logseq_chat/app_test.ml
+++ b/shared/test/logseq_chat/app_test.ml
@@ -8317,7 +8317,7 @@ let composer_asset_failure_retains_draft_and_success_removes_it () =
let composer_asset_schema_matches_native_thumbnail_renderer () =
check_eq ~msg:"native attachment thumbnails share the LG schema"
(Lui_extension.fingerprint (View.composer_asset_schema ()))
- "lui-extension-v1|14:composer-asset|profiles:ios/swiftui|standard-children:0|children:|properties:10:local-path:string:required:none,5:title:string:required:none|events:"
+ "lui-extension-v1|14:composer-asset|profiles:android/flutter,ios/swiftui|standard-children:0|children:|properties:10:local-path:string:required:none,5:title:string:required:none|events:"
let block_node_breadcrumbs_preserve_navigation_context () =
List.iter