Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions flutter/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:label="Logseq Chat"
android:enableOnBackInvokedCallback="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
Expand Down
59 changes: 59 additions & 0 deletions flutter/lib/logseq_chat_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ const _overflowFingerprint =
'properties:14:favorite-label:string:required:none,16:settings-visible:bool:'
'required:none,20:page-actions-visible:bool:required:none|events:5:share[],'
'6:delete[],8:favorite[],8:settings[]';
const _composerAssetFingerprint =
'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:';

String _identityAssetPath(String path) => path;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down
58 changes: 55 additions & 3 deletions flutter/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -47,6 +62,7 @@ class LogseqChatFlutterApp extends StatefulWidget {
class _LogseqChatFlutterAppState extends State<LogseqChatFlutterApp>
with WidgetsBindingObserver {
final _navigatorKey = GlobalKey<NavigatorState>();
final _messengerKey = GlobalKey<ScaffoldMessengerState>();
late final LUIFlutterBackend _backend;
late final FlutterPatchApplier _patchApplier;
LogseqChatNativeBridge? _bridge;
Expand Down Expand Up @@ -602,6 +618,14 @@ class _LogseqChatFlutterAppState extends State<LogseqChatFlutterApp>

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,
Expand Down Expand Up @@ -654,6 +678,7 @@ class _LogseqChatFlutterAppState extends State<LogseqChatFlutterApp>
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
scaffoldMessengerKey: _messengerKey,
debugShowCheckedModeBanner: false,
title: 'Logseq Chat',
theme: LogseqChatTheme.light(),
Expand All @@ -666,9 +691,36 @@ class _LogseqChatFlutterAppState extends State<LogseqChatFlutterApp>
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'),
),
],
),
),
),
);
}
Expand Down
4 changes: 2 additions & 2 deletions flutter/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion shared/src/logseq_chat/model.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion shared/src/logseq_chat/view.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions shared/src/logseq_chat/view_base.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
1 change: 1 addition & 0 deletions shared/src/logseq_chat/view_base.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 42 additions & 1 deletion shared/src/logseq_chat/view_composer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down
60 changes: 55 additions & 5 deletions shared/src/logseq_chat/view_graphs.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
[
Expand Down
1 change: 1 addition & 0 deletions shared/src/logseq_chat/view_graphs.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down
Loading
Loading