mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: collab cursor/selection (#4983)
* feat: support collab selection * feat: collab cusro/selection * chore: add metadata field * feat: support displaying user name above cursor * fix: emit error * feat: support displaying collaborators * feat: sync collaborator * fix: collab doc issues * chore: update deps * feat: refactor device id * chore: enable share button * chore: update collab a816214 * fix: clippy lint * chore: use extension type instead class function * feat: add clear recent views button in debug mode * chore: support clear recent views * feat: support saving the last opened workspace * chore: update collab
This commit is contained in:
parent
bf98a627b9
commit
60acf8c889
@ -64,4 +64,9 @@ class KVKeys {
|
|||||||
/// The value is a json string with the following format:
|
/// The value is a json string with the following format:
|
||||||
/// {'feature_flag_1': true, 'feature_flag_2': false}
|
/// {'feature_flag_1': true, 'feature_flag_2': false}
|
||||||
static const String featureFlag = 'featureFlag';
|
static const String featureFlag = 'featureFlag';
|
||||||
|
|
||||||
|
/// The key for saving the last opened workspace id
|
||||||
|
///
|
||||||
|
/// The workspace id is a string.
|
||||||
|
static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId';
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
|||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/document_sync_indicator.dart';
|
import 'package:appflowy/plugins/document/presentation/document_sync_indicator.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
@ -74,6 +75,14 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||||||
viewPB = view;
|
viewPB = view;
|
||||||
actions.addAll([
|
actions.addAll([
|
||||||
if (FeatureFlag.syncDocument.isOn) ...[
|
if (FeatureFlag.syncDocument.isOn) ...[
|
||||||
|
DocumentCollaborators(
|
||||||
|
width: 60,
|
||||||
|
height: 44,
|
||||||
|
fontSize: 14,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
view: view,
|
||||||
|
),
|
||||||
|
const HSpace(16.0),
|
||||||
DocumentSyncIndicator(view: view),
|
DocumentSyncIndicator(view: view),
|
||||||
const HSpace(8.0),
|
const HSpace(8.0),
|
||||||
],
|
],
|
||||||
|
@ -94,6 +94,9 @@ class MobileHomePage extends StatelessWidget {
|
|||||||
previous.currentWorkspace?.workspaceId !=
|
previous.currentWorkspace?.workspaceId !=
|
||||||
current.currentWorkspace?.workspaceId,
|
current.currentWorkspace?.workspaceId,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
if (state.currentWorkspace == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
|
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
@ -7,6 +10,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class MobileRecentFolder extends StatefulWidget {
|
class MobileRecentFolder extends StatefulWidget {
|
||||||
const MobileRecentFolder({super.key});
|
const MobileRecentFolder({super.key});
|
||||||
@ -76,11 +80,70 @@ class _RecentViews extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: GestureDetector(
|
||||||
child: FlowyText.semibold(
|
child: FlowyText.semibold(
|
||||||
LocaleKeys.sideBar_recent.tr(),
|
LocaleKeys.sideBar_recent.tr(),
|
||||||
fontSize: 20.0,
|
fontSize: 20.0,
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
showMobileBottomSheet(
|
||||||
|
context,
|
||||||
|
showDivider: false,
|
||||||
|
showDragHandle: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
builder: (_) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
FlowyOptionTile.text(
|
||||||
|
text: LocaleKeys.button_clear.tr(),
|
||||||
|
leftIcon: FlowySvg(
|
||||||
|
FlowySvgs.m_delete_s,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
|
textColor: Theme.of(context).colorScheme.error,
|
||||||
|
onTap: () {
|
||||||
|
context.read<RecentViewsBloc>().add(
|
||||||
|
RecentViewsEvent.removeRecentViews(
|
||||||
|
recentViews.map((e) => e.id).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Row(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// children: [
|
||||||
|
// Padding(
|
||||||
|
// padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
// child: FlowyText.semibold(
|
||||||
|
// LocaleKeys.sideBar_recent.tr(),
|
||||||
|
// fontSize: 20.0,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// if (kDebugMode)
|
||||||
|
// Padding(
|
||||||
|
// padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
// child: FlowyButton(
|
||||||
|
// useIntrinsicWidth: true,
|
||||||
|
// text: FlowyText(LocaleKeys.button_clear.tr()),
|
||||||
|
// onTap: () {
|
||||||
|
// context.read<RecentViewsBloc>().add(
|
||||||
|
// RecentViewsEvent.removeRecentViews(
|
||||||
|
// recentViews.map((e) => e.id).toList(),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
key: const PageStorageKey('recent_views_page_storage_key'),
|
key: const PageStorageKey('recent_views_page_storage_key'),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
@ -2,10 +2,10 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_listener.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/shared/appflowy_network_image.dart';
|
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||||
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
@ -53,7 +53,7 @@ class _MobileRecentViewState extends State<MobileRecentView> {
|
|||||||
|
|
||||||
documentListener = DocumentListener(id: view.id)
|
documentListener = DocumentListener(id: view.id)
|
||||||
..start(
|
..start(
|
||||||
didReceiveUpdate: (document) {
|
onDocEventUpdate: (document) {
|
||||||
setState(() {
|
setState(() {
|
||||||
view = view;
|
view = view;
|
||||||
});
|
});
|
||||||
|
@ -47,7 +47,7 @@ class AboutSettingGroup extends StatelessWidget {
|
|||||||
MobileSettingItem(
|
MobileSettingItem(
|
||||||
name: LocaleKeys.settings_mobile_version.tr(),
|
name: LocaleKeys.settings_mobile_version.tr(),
|
||||||
trailing: FlowyText(
|
trailing: FlowyText(
|
||||||
'${DeviceOrApplicationInfoTask.applicationVersion} (${DeviceOrApplicationInfoTask.buildNumber})',
|
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc
|
|||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
class SelfHostUrlBottomSheet extends StatefulWidget {
|
class SelfHostUrlBottomSheet extends StatefulWidget {
|
||||||
const SelfHostUrlBottomSheet({
|
const SelfHostUrlBottomSheet({
|
||||||
@ -38,32 +37,9 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
LocaleKeys.editor_urlHint.tr(),
|
|
||||||
style: theme.textTheme.labelSmall,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: theme.hintColor,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Form(
|
Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
|
@ -36,6 +36,14 @@ class _SelfHostSettingGroupState extends State<SelfHostSettingGroup> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
showMobileBottomSheet(
|
showMobileBottomSheet(
|
||||||
context,
|
context,
|
||||||
|
showHeader: true,
|
||||||
|
title: LocaleKeys.editor_urlHint.tr(),
|
||||||
|
showCloseButton: true,
|
||||||
|
showDivider: false,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return SelfHostUrlBottomSheet(
|
return SelfHostUrlBottomSheet(
|
||||||
url: url,
|
url: url,
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
// This file is "main.dart"
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'doc_awareness_metadata.freezed.dart';
|
||||||
|
part 'doc_awareness_metadata.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DocumentAwarenessMetadata with _$DocumentAwarenessMetadata {
|
||||||
|
const factory DocumentAwarenessMetadata({
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonKey(name: 'cursor_color') required String cursorColor,
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonKey(name: 'selection_color') required String selectionColor,
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonKey(name: 'user_name') required String userName,
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonKey(name: 'user_avatar') required String userAvatar,
|
||||||
|
}) = _DocumentAwarenessMetadata;
|
||||||
|
|
||||||
|
factory DocumentAwarenessMetadata.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$DocumentAwarenessMetadataFromJson(json);
|
||||||
|
}
|
@ -1,15 +1,21 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/application/collab_document_adapter.dart';
|
import 'package:appflowy/plugins/document/application/doc_awareness_metadata.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_collab_adapter.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_listener.dart';
|
||||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||||
import 'package:appflowy/workspace/application/doc/sync_state_listener.dart';
|
import 'package:appflowy/util/color_to_hex_string.dart';
|
||||||
|
import 'package:appflowy/util/debounce.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||||
@ -50,14 +56,17 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
final DocumentService _documentService = DocumentService();
|
final DocumentService _documentService = DocumentService();
|
||||||
final TrashService _trashService = TrashService();
|
final TrashService _trashService = TrashService();
|
||||||
|
|
||||||
late CollabDocumentAdapter _collabDocumentAdapter;
|
late DocumentCollabAdapter _documentCollabAdapter;
|
||||||
|
|
||||||
late final TransactionAdapter _transactionAdapter = TransactionAdapter(
|
late final TransactionAdapter _transactionAdapter = TransactionAdapter(
|
||||||
documentId: view.id,
|
documentId: view.id,
|
||||||
documentService: _documentService,
|
documentService: _documentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
StreamSubscription? _subscription;
|
StreamSubscription? _transactionSubscription;
|
||||||
|
|
||||||
|
final _updateSelectionDebounce = Debounce();
|
||||||
|
final _syncDocDebounce = Debounce();
|
||||||
|
|
||||||
bool get isLocalMode {
|
bool get isLocalMode {
|
||||||
final userProfilePB = state.userProfilePB;
|
final userProfilePB = state.userProfilePB;
|
||||||
@ -70,7 +79,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
await _documentListener.stop();
|
await _documentListener.stop();
|
||||||
await _syncStateListener.stop();
|
await _syncStateListener.stop();
|
||||||
await _viewListener.stop();
|
await _viewListener.stop();
|
||||||
await _subscription?.cancel();
|
await _transactionSubscription?.cancel();
|
||||||
await _documentService.closeDocument(view: view);
|
await _documentService.closeDocument(view: view);
|
||||||
state.editorState?.service.keyboardService?.closeKeyboard();
|
state.editorState?.service.keyboardService?.closeKeyboard();
|
||||||
state.editorState?.dispose();
|
state.editorState?.dispose();
|
||||||
@ -104,6 +113,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
emit(newState);
|
emit(newState);
|
||||||
|
if (newState.userProfilePB != null) {
|
||||||
|
await _updateCollaborator();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moveToTrash: () async {
|
moveToTrash: () async {
|
||||||
emit(state.copyWith(isDeleted: true));
|
emit(state.copyWith(isDeleted: true));
|
||||||
@ -143,7 +155,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
/// subscribe to the document content change
|
/// subscribe to the document content change
|
||||||
void _onDocumentChanged() {
|
void _onDocumentChanged() {
|
||||||
_documentListener.start(
|
_documentListener.start(
|
||||||
didReceiveUpdate: syncDocumentDataPB,
|
onDocEventUpdate: _debounceSyncDoc,
|
||||||
|
onDocAwarenessUpdate: _onAwarenessStatesUpdate,
|
||||||
);
|
);
|
||||||
|
|
||||||
_syncStateListener.start(
|
_syncStateListener.start(
|
||||||
@ -173,24 +186,31 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
|
|
||||||
final editorState = EditorState(document: document);
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
_collabDocumentAdapter = CollabDocumentAdapter(editorState, view.id);
|
_documentCollabAdapter = DocumentCollabAdapter(editorState, view.id);
|
||||||
|
|
||||||
// subscribe to the document change from the editor
|
// subscribe to the document change from the editor
|
||||||
_subscription = editorState.transactionStream.listen((event) async {
|
_transactionSubscription = editorState.transactionStream.listen(
|
||||||
|
(event) async {
|
||||||
final time = event.$1;
|
final time = event.$1;
|
||||||
|
final transaction = event.$2;
|
||||||
if (time != TransactionTime.before) {
|
if (time != TransactionTime.before) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _transactionAdapter.apply(event.$2, editorState);
|
|
||||||
|
// apply transaction to backend
|
||||||
|
await _transactionAdapter.apply(transaction, editorState);
|
||||||
|
|
||||||
// check if the document is empty.
|
// check if the document is empty.
|
||||||
await applyRules();
|
await _applyRules();
|
||||||
|
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
// ignore: invalid_use_of_visible_for_testing_member
|
||||||
emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty));
|
emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty));
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
editorState.selectionNotifier.addListener(_debounceOnSelectionUpdate);
|
||||||
|
|
||||||
// output the log from the editor when debug mode
|
// output the log from the editor when debug mode
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
@ -204,14 +224,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
return editorState;
|
return editorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> applyRules() async {
|
Future<void> _applyRules() async {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
ensureAtLeastOneParagraphExists(),
|
_ensureAtLeastOneParagraphExists(),
|
||||||
ensureLastNodeIsEditable(),
|
_ensureLastNodeIsEditable(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> ensureLastNodeIsEditable() async {
|
Future<void> _ensureLastNodeIsEditable() async {
|
||||||
final editorState = state.editorState;
|
final editorState = state.editorState;
|
||||||
if (editorState == null) {
|
if (editorState == null) {
|
||||||
return;
|
return;
|
||||||
@ -226,7 +246,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> ensureAtLeastOneParagraphExists() async {
|
Future<void> _ensureAtLeastOneParagraphExists() async {
|
||||||
final editorState = state.editorState;
|
final editorState = state.editorState;
|
||||||
if (editorState == null) {
|
if (editorState == null) {
|
||||||
return;
|
return;
|
||||||
@ -242,12 +262,89 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> syncDocumentDataPB(DocEventPB docEvent) async {
|
Future<void> _onDocumentStateUpdate(DocEventPB docEvent) async {
|
||||||
if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) {
|
if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _collabDocumentAdapter.syncV3();
|
unawaited(_documentCollabAdapter.syncV3(docEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onAwarenessStatesUpdate(
|
||||||
|
DocumentAwarenessStatesPB awarenessStates,
|
||||||
|
) async {
|
||||||
|
if (!FeatureFlag.syncDocument.isOn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = state.userProfilePB?.id;
|
||||||
|
if (userId != null) {
|
||||||
|
await _documentCollabAdapter.updateRemoteSelection(
|
||||||
|
userId.toString(),
|
||||||
|
awarenessStates,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _debounceOnSelectionUpdate() {
|
||||||
|
_updateSelectionDebounce.call(_onSelectionUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _debounceSyncDoc(DocEventPB docEvent) {
|
||||||
|
_syncDocDebounce.call(() {
|
||||||
|
_onDocumentStateUpdate(docEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSelectionUpdate() async {
|
||||||
|
final user = state.userProfilePB;
|
||||||
|
final deviceId = ApplicationInfo.deviceId;
|
||||||
|
if (!FeatureFlag.syncDocument.isOn || user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final editorState = state.editorState;
|
||||||
|
if (editorState == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final selection = editorState.selection;
|
||||||
|
|
||||||
|
// sync the selection
|
||||||
|
final id = user.id.toString() + deviceId;
|
||||||
|
final basicColor = ColorGenerator(id.toString()).toColor();
|
||||||
|
final metadata = DocumentAwarenessMetadata(
|
||||||
|
cursorColor: basicColor.toHexString(),
|
||||||
|
selectionColor: basicColor.withOpacity(0.6).toHexString(),
|
||||||
|
userName: user.name,
|
||||||
|
userAvatar: user.iconUrl,
|
||||||
|
);
|
||||||
|
await _documentService.syncAwarenessStates(
|
||||||
|
documentId: view.id,
|
||||||
|
selection: selection,
|
||||||
|
metadata: jsonEncode(metadata.toJson()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateCollaborator() async {
|
||||||
|
final user = state.userProfilePB;
|
||||||
|
final deviceId = ApplicationInfo.deviceId;
|
||||||
|
if (!FeatureFlag.syncDocument.isOn || user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync the selection
|
||||||
|
final id = user.id.toString() + deviceId;
|
||||||
|
final basicColor = ColorGenerator(id.toString()).toColor();
|
||||||
|
final metadata = DocumentAwarenessMetadata(
|
||||||
|
cursorColor: basicColor.toHexString(),
|
||||||
|
selectionColor: basicColor.withOpacity(0.6).toHexString(),
|
||||||
|
userName: user.name,
|
||||||
|
userAvatar: user.iconUrl,
|
||||||
|
);
|
||||||
|
await _documentService.syncAwarenessStates(
|
||||||
|
documentId: view.id,
|
||||||
|
metadata: jsonEncode(metadata.toJson()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +371,7 @@ class DocumentState with _$DocumentState {
|
|||||||
UserProfilePB? userProfilePB,
|
UserProfilePB? userProfilePB,
|
||||||
EditorState? editorState,
|
EditorState? editorState,
|
||||||
FlowyError? error,
|
FlowyError? error,
|
||||||
|
@Default(null) DocumentAwarenessStatesPB? awarenessStates,
|
||||||
}) = _DocumentState;
|
}) = _DocumentState;
|
||||||
|
|
||||||
factory DocumentState.initial() => const DocumentState(
|
factory DocumentState.initial() => const DocumentState(
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_awareness_metadata.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
|
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||||
|
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||||
import 'package:appflowy/util/json_print.dart';
|
import 'package:appflowy/util/json_print.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CollabDocumentAdapter {
|
class DocumentCollabAdapter {
|
||||||
CollabDocumentAdapter(this.editorState, this.docId);
|
DocumentCollabAdapter(this.editorState, this.docId);
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final String docId;
|
final String docId;
|
||||||
@ -61,7 +67,7 @@ class CollabDocumentAdapter {
|
|||||||
/// Sync version 3
|
/// Sync version 3
|
||||||
///
|
///
|
||||||
/// Diff the local document with the remote document and apply the changes
|
/// Diff the local document with the remote document and apply the changes
|
||||||
Future<void> syncV3() async {
|
Future<void> syncV3(DocEventPB docEvent) async {
|
||||||
final result = await _service.getDocument(viewId: docId);
|
final result = await _service.getDocument(viewId: docId);
|
||||||
final document = result.fold((s) => s.toDocument(), (f) => null);
|
final document = result.fold((s) => s.toDocument(), (f) => null);
|
||||||
if (document == null) {
|
if (document == null) {
|
||||||
@ -70,9 +76,12 @@ class CollabDocumentAdapter {
|
|||||||
|
|
||||||
final ops = diffNodes(editorState.document.root, document.root);
|
final ops = diffNodes(editorState.document.root, document.root);
|
||||||
if (ops.isEmpty) {
|
if (ops.isEmpty) {
|
||||||
|
debugPrint('[collab] received empty ops');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugPrint('[collab] received ops: $ops');
|
||||||
|
|
||||||
final transaction = editorState.transaction;
|
final transaction = editorState.transaction;
|
||||||
for (final op in ops) {
|
for (final op in ops) {
|
||||||
transaction.add(op);
|
transaction.add(op);
|
||||||
@ -122,6 +131,80 @@ class CollabDocumentAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateRemoteSelection(
|
||||||
|
String userId,
|
||||||
|
DocumentAwarenessStatesPB states,
|
||||||
|
) async {
|
||||||
|
final List<RemoteSelection> remoteSelections = [];
|
||||||
|
final deviceId = ApplicationInfo.deviceId;
|
||||||
|
for (final state in states.value.values) {
|
||||||
|
// the following code is only for version 1
|
||||||
|
if (state.version != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final uid = state.user.uid.toString();
|
||||||
|
final did = state.user.deviceId;
|
||||||
|
final metadata = DocumentAwarenessMetadata.fromJson(
|
||||||
|
jsonDecode(state.metadata),
|
||||||
|
);
|
||||||
|
final selectionColor = metadata.selectionColor.tryToColor();
|
||||||
|
final cursorColor = metadata.cursorColor.tryToColor();
|
||||||
|
if ((uid == userId && did == deviceId) ||
|
||||||
|
(cursorColor == null || selectionColor == null)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final start = state.selection.start;
|
||||||
|
final end = state.selection.end;
|
||||||
|
final selection = Selection(
|
||||||
|
start: Position(
|
||||||
|
path: start.path.toIntList(),
|
||||||
|
offset: start.offset.toInt(),
|
||||||
|
),
|
||||||
|
end: Position(
|
||||||
|
path: end.path.toIntList(),
|
||||||
|
offset: end.offset.toInt(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final color = ColorGenerator(uid + did).toColor();
|
||||||
|
final remoteSelection = RemoteSelection(
|
||||||
|
id: uid,
|
||||||
|
selection: selection,
|
||||||
|
selectionColor: selectionColor,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
builder: (_, __, rect) {
|
||||||
|
return Positioned(
|
||||||
|
top: rect.top - 10,
|
||||||
|
left: selection.isCollapsed ? rect.right : rect.left,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: color,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 2.0,
|
||||||
|
vertical: 1.0,
|
||||||
|
),
|
||||||
|
child: FlowyText(
|
||||||
|
metadata.userName,
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 12.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
remoteSelections.add(remoteSelection);
|
||||||
|
}
|
||||||
|
if (remoteSelections.isNotEmpty) {
|
||||||
|
editorState.remoteSelections.value = remoteSelections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on List<Int64> {
|
||||||
|
List<int> toIntList() {
|
||||||
|
return map((e) => e.toInt()).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on List<String> {
|
extension on List<String> {
|
@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_awareness_metadata.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_listener.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||||
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'doc_collaborators_bloc.freezed.dart';
|
||||||
|
|
||||||
|
bool _filterCurrentUser = false;
|
||||||
|
|
||||||
|
class DocumentCollaboratorsBloc
|
||||||
|
extends Bloc<DocumentCollaboratorsEvent, DocumentCollaboratorsState> {
|
||||||
|
DocumentCollaboratorsBloc({
|
||||||
|
required this.view,
|
||||||
|
}) : _listener = DocumentListener(id: view.id),
|
||||||
|
super(DocumentCollaboratorsState.initial()) {
|
||||||
|
on<DocumentCollaboratorsEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {
|
||||||
|
final result = await getIt<AuthService>().getUser();
|
||||||
|
final userProfile = result.fold((s) => s, (f) => null);
|
||||||
|
final deviceId = ApplicationInfo.deviceId;
|
||||||
|
_listener.start(
|
||||||
|
onDocAwarenessUpdate: (states) {
|
||||||
|
if (userProfile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
add(
|
||||||
|
DocumentCollaboratorsEvent.update(
|
||||||
|
userProfile,
|
||||||
|
deviceId,
|
||||||
|
states,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
update: (userProfile, deviceId, states) {
|
||||||
|
final collaborators = _buildCollaborators(
|
||||||
|
userProfile,
|
||||||
|
deviceId,
|
||||||
|
states,
|
||||||
|
);
|
||||||
|
emit(state.copyWith(collaborators: collaborators));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
final DocumentListener _listener;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _listener.stop();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DocumentAwarenessMetadata> _buildCollaborators(
|
||||||
|
UserProfilePB userProfile,
|
||||||
|
String deviceId,
|
||||||
|
DocumentAwarenessStatesPB states,
|
||||||
|
) {
|
||||||
|
final result = <DocumentAwarenessMetadata>[];
|
||||||
|
final ids = <dynamic>{};
|
||||||
|
final sorted = states.value.values.toList()
|
||||||
|
..sort((a, b) => b.timestamp.compareTo(a.timestamp))
|
||||||
|
..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId));
|
||||||
|
for (final state in sorted) {
|
||||||
|
if (state.version != 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// filter current user
|
||||||
|
if (_filterCurrentUser &&
|
||||||
|
userProfile.id == state.user.uid &&
|
||||||
|
deviceId == state.user.deviceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final metadata = DocumentAwarenessMetadata.fromJson(
|
||||||
|
jsonDecode(state.metadata),
|
||||||
|
);
|
||||||
|
result.add(metadata);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to parse metadata: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DocumentCollaboratorsEvent with _$DocumentCollaboratorsEvent {
|
||||||
|
const factory DocumentCollaboratorsEvent.initial() = Initial;
|
||||||
|
const factory DocumentCollaboratorsEvent.update(
|
||||||
|
UserProfilePB userProfile,
|
||||||
|
String deviceId,
|
||||||
|
DocumentAwarenessStatesPB states,
|
||||||
|
) = Update;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DocumentCollaboratorsState with _$DocumentCollaboratorsState {
|
||||||
|
const factory DocumentCollaboratorsState({
|
||||||
|
@Default([]) List<DocumentAwarenessMetadata> collaborators,
|
||||||
|
}) = _DocumentCollaboratorsState;
|
||||||
|
|
||||||
|
factory DocumentCollaboratorsState.initial() =>
|
||||||
|
const DocumentCollaboratorsState();
|
||||||
|
}
|
@ -8,6 +8,11 @@ import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
|
|||||||
import 'package:appflowy_backend/rust_stream.dart';
|
import 'package:appflowy_backend/rust_stream.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
|
|
||||||
|
typedef OnDocumentEventUpdate = void Function(DocEventPB docEvent);
|
||||||
|
typedef OnDocumentAwarenessStateUpdate = void Function(
|
||||||
|
DocumentAwarenessStatesPB awarenessStates,
|
||||||
|
);
|
||||||
|
|
||||||
class DocumentListener {
|
class DocumentListener {
|
||||||
DocumentListener({
|
DocumentListener({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -18,12 +23,15 @@ class DocumentListener {
|
|||||||
StreamSubscription<SubscribeObject>? _subscription;
|
StreamSubscription<SubscribeObject>? _subscription;
|
||||||
DocumentNotificationParser? _parser;
|
DocumentNotificationParser? _parser;
|
||||||
|
|
||||||
Function(DocEventPB docEvent)? didReceiveUpdate;
|
OnDocumentEventUpdate? _onDocEventUpdate;
|
||||||
|
OnDocumentAwarenessStateUpdate? _onDocAwarenessUpdate;
|
||||||
|
|
||||||
void start({
|
void start({
|
||||||
Function(DocEventPB docEvent)? didReceiveUpdate,
|
OnDocumentEventUpdate? onDocEventUpdate,
|
||||||
|
OnDocumentAwarenessStateUpdate? onDocAwarenessUpdate,
|
||||||
}) {
|
}) {
|
||||||
this.didReceiveUpdate = didReceiveUpdate;
|
_onDocEventUpdate = onDocEventUpdate;
|
||||||
|
_onDocAwarenessUpdate = onDocAwarenessUpdate;
|
||||||
|
|
||||||
_parser = DocumentNotificationParser(
|
_parser = DocumentNotificationParser(
|
||||||
id: id,
|
id: id,
|
||||||
@ -40,7 +48,16 @@ class DocumentListener {
|
|||||||
) {
|
) {
|
||||||
switch (ty) {
|
switch (ty) {
|
||||||
case DocumentNotification.DidReceiveUpdate:
|
case DocumentNotification.DidReceiveUpdate:
|
||||||
result.map((r) => didReceiveUpdate?.call(DocEventPB.fromBuffer(r)));
|
result.map(
|
||||||
|
(s) => _onDocEventUpdate?.call(DocEventPB.fromBuffer(s)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case DocumentNotification.DidUpdateDocumentAwarenessState:
|
||||||
|
result.map(
|
||||||
|
(s) => _onDocAwarenessUpdate?.call(
|
||||||
|
DocumentAwarenessStatesPB.fromBuffer(s),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -48,6 +65,8 @@ class DocumentListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
|
_onDocAwarenessUpdate = null;
|
||||||
|
_onDocEventUpdate = null;
|
||||||
await _subscription?.cancel();
|
await _subscription?.cancel();
|
||||||
_subscription = null;
|
_subscription = null;
|
||||||
}
|
}
|
@ -2,7 +2,9 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
||||||
class DocumentService {
|
class DocumentService {
|
||||||
// unused now.
|
// unused now.
|
||||||
@ -143,4 +145,39 @@ class DocumentService {
|
|||||||
return FlowyResult.failure(FlowyError(msg: 'Workspace not found'));
|
return FlowyResult.failure(FlowyError(msg: 'Workspace not found'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync the awareness states
|
||||||
|
/// For example, the cursor position, selection, who is viewing the document.
|
||||||
|
Future<FlowyResult<void, FlowyError>> syncAwarenessStates({
|
||||||
|
required String documentId,
|
||||||
|
Selection? selection,
|
||||||
|
String? metadata,
|
||||||
|
}) async {
|
||||||
|
final payload = UpdateDocumentAwarenessStatePB(
|
||||||
|
documentId: documentId,
|
||||||
|
selection: convertSelectionToAwarenessSelection(selection),
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await DocumentEventSetAwarenessState(payload).send();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAwarenessSelectionPB? convertSelectionToAwarenessSelection(
|
||||||
|
Selection? selection,
|
||||||
|
) {
|
||||||
|
if (selection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return DocumentAwarenessSelectionPB(
|
||||||
|
start: DocumentAwarenessPositionPB(
|
||||||
|
offset: Int64(selection.startIndex),
|
||||||
|
path: selection.start.path.map((e) => Int64(e)),
|
||||||
|
),
|
||||||
|
end: DocumentAwarenessPositionPB(
|
||||||
|
offset: Int64(selection.endIndex),
|
||||||
|
path: selection.end.path.map((e) => Int64(e)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/doc/sync_state_listener.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
@ -49,12 +49,13 @@ class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
|
|||||||
|
|
||||||
connectivityStream =
|
connectivityStream =
|
||||||
_connectivity.onConnectivityChanged.listen((result) {
|
_connectivity.onConnectivityChanged.listen((result) {
|
||||||
if (!isClosed) {}
|
if (!isClosed) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isNetworkConnected: result != ConnectivityResult.none,
|
isNetworkConnected: result != ConnectivityResult.none,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
syncStateChanged: (syncState) {
|
syncStateChanged: (syncState) {
|
||||||
|
@ -144,10 +144,11 @@ extension BlockToNode on BlockPB {
|
|||||||
final deltaString = meta.textMap[externalId];
|
final deltaString = meta.textMap[externalId];
|
||||||
if (deltaString != null) {
|
if (deltaString != null) {
|
||||||
final delta = jsonDecode(deltaString);
|
final delta = jsonDecode(deltaString);
|
||||||
map.putIfAbsent(
|
map['delta'] = delta;
|
||||||
'delta',
|
// map.putIfAbsent(
|
||||||
() => delta,
|
// 'delta',
|
||||||
);
|
// () => delta,
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/plugins/document/document_page.dart';
|
import 'package:appflowy/plugins/document/document_page.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/document_sync_indicator.dart';
|
import 'package:appflowy/plugins/document/presentation/document_sync_indicator.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||||
import 'package:appflowy/plugins/util.dart';
|
import 'package:appflowy/plugins/util.dart';
|
||||||
@ -140,20 +141,27 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
DocumentShareButton(
|
|
||||||
key: ValueKey('share_button_${view.id}'),
|
|
||||||
view: view,
|
|
||||||
),
|
|
||||||
...FeatureFlag.syncDocument.isOn
|
...FeatureFlag.syncDocument.isOn
|
||||||
? [
|
? [
|
||||||
const HSpace(20),
|
DocumentCollaborators(
|
||||||
|
key: ValueKey('collaborators_${view.id}'),
|
||||||
|
width: 100,
|
||||||
|
height: 32,
|
||||||
|
view: view,
|
||||||
|
),
|
||||||
|
const HSpace(16),
|
||||||
DocumentSyncIndicator(
|
DocumentSyncIndicator(
|
||||||
key: ValueKey('sync_state_${view.id}'),
|
key: ValueKey('sync_state_${view.id}'),
|
||||||
view: view,
|
view: view,
|
||||||
),
|
),
|
||||||
const HSpace(12),
|
const HSpace(16),
|
||||||
]
|
]
|
||||||
: [const HSpace(8)],
|
: [const HSpace(8)],
|
||||||
|
DocumentShareButton(
|
||||||
|
key: ValueKey('share_button_${view.id}'),
|
||||||
|
view: view,
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
ViewFavoriteButton(
|
ViewFavoriteButton(
|
||||||
key: ValueKey('favorite_button_${view.id}'),
|
key: ValueKey('favorite_button_${view.id}'),
|
||||||
view: view,
|
view: view,
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:avatar_stack/avatar_stack.dart';
|
||||||
|
import 'package:avatar_stack/positions.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CollaboratorAvatarStack extends StatelessWidget {
|
||||||
|
const CollaboratorAvatarStack({
|
||||||
|
super.key,
|
||||||
|
required this.avatars,
|
||||||
|
this.settings,
|
||||||
|
this.infoWidgetBuilder,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.borderWidth,
|
||||||
|
this.borderColor,
|
||||||
|
this.backgroundColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> avatars;
|
||||||
|
|
||||||
|
final Positions? settings;
|
||||||
|
|
||||||
|
final InfoWidgetBuilder? infoWidgetBuilder;
|
||||||
|
|
||||||
|
final double? width;
|
||||||
|
|
||||||
|
final double? height;
|
||||||
|
|
||||||
|
final double? borderWidth;
|
||||||
|
|
||||||
|
final Color? borderColor;
|
||||||
|
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final settings = this.settings ??
|
||||||
|
RestrictedPositions(
|
||||||
|
maxCoverage: 0.3,
|
||||||
|
minCoverage: 0.1,
|
||||||
|
align: StackAlign.right,
|
||||||
|
);
|
||||||
|
|
||||||
|
final border = BorderSide(
|
||||||
|
color: borderColor ?? Theme.of(context).colorScheme.onPrimary,
|
||||||
|
width: borderWidth ?? 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget textInfoWidgetBuilder(surplus) => BorderedCircleAvatar(
|
||||||
|
border: border,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
child: FittedBox(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'+$surplus',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final infoWidgetBuilder = this.infoWidgetBuilder ?? textInfoWidgetBuilder;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
child: WidgetStack(
|
||||||
|
positions: settings,
|
||||||
|
buildInfoWidget: infoWidgetBuilder,
|
||||||
|
stackedWidgets: avatars
|
||||||
|
.map(
|
||||||
|
(avatar) => CircleAvatar(
|
||||||
|
backgroundColor: border.color,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(border.width),
|
||||||
|
child: avatar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
import 'package:appflowy/plugins/document/application/doc_collaborators_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/collaborator_avater_stack.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class DocumentCollaborators extends StatelessWidget {
|
||||||
|
const DocumentCollaborators({
|
||||||
|
super.key,
|
||||||
|
required this.height,
|
||||||
|
required this.width,
|
||||||
|
required this.view,
|
||||||
|
this.padding,
|
||||||
|
this.fontSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
final double height;
|
||||||
|
final double width;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final double? fontSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => DocumentCollaboratorsBloc(view: view)
|
||||||
|
..add(const DocumentCollaboratorsEvent.initial()),
|
||||||
|
child: BlocBuilder<DocumentCollaboratorsBloc, DocumentCollaboratorsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final collaborators = state.collaborators;
|
||||||
|
if (collaborators.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: padding ?? EdgeInsets.zero,
|
||||||
|
child: CollaboratorAvatarStack(
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
borderWidth: 1.0,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
avatars: collaborators
|
||||||
|
.map(
|
||||||
|
(c) => FlowyTooltip(
|
||||||
|
message: c.userName,
|
||||||
|
child: CircleAvatar(
|
||||||
|
backgroundColor: c.selectionColor.tryToColor(),
|
||||||
|
child: FlowyText(
|
||||||
|
c.userName.characters.firstOrNull ?? ' ',
|
||||||
|
fontSize: fontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -35,7 +35,7 @@ class KeyboardHeightObserver {
|
|||||||
|
|
||||||
void notify(double height) {
|
void notify(double height) {
|
||||||
// the keyboard height will notify twice with the same value on Android 14
|
// the keyboard height will notify twice with the same value on Android 14
|
||||||
if (DeviceOrApplicationInfoTask.androidSDKVersion == 34) {
|
if (ApplicationInfo.androidSDKVersion == 34) {
|
||||||
if (height == 0 && currentKeyboardHeight == 0) {
|
if (height == 0 && currentKeyboardHeight == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ enum FeatureFlag {
|
|||||||
case FeatureFlag.membersSettings:
|
case FeatureFlag.membersSettings:
|
||||||
return false;
|
return false;
|
||||||
case FeatureFlag.syncDocument:
|
case FeatureFlag.syncDocument:
|
||||||
return false;
|
return true;
|
||||||
case FeatureFlag.unknown:
|
case FeatureFlag.unknown:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,7 @@ class FlowyRunner {
|
|||||||
if (!mode.isUnitTest) ...[
|
if (!mode.isUnitTest) ...[
|
||||||
// The DeviceOrApplicationInfoTask should be placed before the AppWidgetTask to fetch the app information.
|
// The DeviceOrApplicationInfoTask should be placed before the AppWidgetTask to fetch the app information.
|
||||||
// It is unable to get the device information from the test environment.
|
// It is unable to get the device information from the test environment.
|
||||||
const DeviceOrApplicationInfoTask(),
|
const ApplicationInfoTask(),
|
||||||
const HotKeyTask(),
|
const HotKeyTask(),
|
||||||
if (isSupabaseEnabled) InitSupabaseTask(),
|
if (isSupabaseEnabled) InitSupabaseTask(),
|
||||||
if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(),
|
if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(),
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
import '../startup.dart';
|
import '../startup.dart';
|
||||||
|
|
||||||
class DeviceOrApplicationInfoTask extends LaunchTask {
|
class ApplicationInfo {
|
||||||
const DeviceOrApplicationInfoTask();
|
|
||||||
|
|
||||||
static int androidSDKVersion = -1;
|
static int androidSDKVersion = -1;
|
||||||
static String applicationVersion = '';
|
static String applicationVersion = '';
|
||||||
static String buildNumber = '';
|
static String buildNumber = '';
|
||||||
|
static String deviceId = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApplicationInfoTask extends LaunchTask {
|
||||||
|
const ApplicationInfoTask();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> initialize(LaunchContext context) async {
|
Future<void> initialize(LaunchContext context) async {
|
||||||
@ -19,13 +23,41 @@ class DeviceOrApplicationInfoTask extends LaunchTask {
|
|||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final androidInfo = await deviceInfoPlugin.androidInfo;
|
final androidInfo = await deviceInfoPlugin.androidInfo;
|
||||||
androidSDKVersion = androidInfo.version.sdkInt;
|
ApplicationInfo.androidSDKVersion = androidInfo.version.sdkInt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.isAndroid || Platform.isIOS) {
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
applicationVersion = packageInfo.version;
|
ApplicationInfo.applicationVersion = packageInfo.version;
|
||||||
buildNumber = packageInfo.buildNumber;
|
ApplicationInfo.buildNumber = packageInfo.buildNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? deviceId;
|
||||||
|
try {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final AndroidDeviceInfo androidInfo =
|
||||||
|
await deviceInfoPlugin.androidInfo;
|
||||||
|
deviceId = androidInfo.device;
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
||||||
|
deviceId = iosInfo.identifierForVendor;
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
final MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo;
|
||||||
|
deviceId = macInfo.systemGUID;
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
final WindowsDeviceInfo windowsInfo =
|
||||||
|
await deviceInfoPlugin.windowsInfo;
|
||||||
|
deviceId = windowsInfo.deviceId;
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
final LinuxDeviceInfo linuxInfo = await deviceInfoPlugin.linuxInfo;
|
||||||
|
deviceId = linuxInfo.machineId;
|
||||||
|
} else {
|
||||||
|
deviceId = null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to get platform version, $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationInfo.deviceId = deviceId ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -25,7 +25,7 @@ Future<String> getDeviceId() async {
|
|||||||
deviceId = macInfo.systemGUID;
|
deviceId = macInfo.systemGUID;
|
||||||
} else if (Platform.isWindows) {
|
} else if (Platform.isWindows) {
|
||||||
final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo;
|
final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo;
|
||||||
deviceId = windowsInfo.computerName;
|
deviceId = windowsInfo.deviceId;
|
||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;
|
final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;
|
||||||
deviceId = linuxInfo.machineId;
|
deviceId = linuxInfo.machineId;
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ColorGenerator {
|
extension type ColorGenerator(String value) {
|
||||||
static Color generateColorFromString(String string) {
|
Color toColor() {
|
||||||
final int hash =
|
final int hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit);
|
||||||
string.codeUnits.fold(0, (int acc, int unit) => acc + unit);
|
|
||||||
final double hue = (hash % 360).toDouble();
|
final double hue = (hash % 360).toDouble();
|
||||||
return HSLColor.fromAHSL(1.0, hue, 0.5, 0.8).toColor();
|
return HSLColor.fromAHSL(1.0, hue, 0.5, 0.8).toColor();
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension ColorExtensionn on Color {
|
extension ColorExtension on Color {
|
||||||
/// return a hex string in 0xff000000 format
|
/// return a hex string in 0xff000000 format
|
||||||
String toHexString() {
|
String toHexString() {
|
||||||
return '0x${value.toRadixString(16).padLeft(8, '0')}';
|
return '0x${value.toRadixString(16).padLeft(8, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// return a random color
|
||||||
|
static Color random({double opacity = 1.0}) {
|
||||||
|
return Color((math.Random().nextDouble() * 0xFFFFFF).toInt())
|
||||||
|
.withOpacity(opacity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'package:appflowy/core/config/kv.dart';
|
||||||
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||||
@ -135,6 +138,12 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||||||
),
|
),
|
||||||
(e) => state.currentWorkspace,
|
(e) => state.currentWorkspace,
|
||||||
);
|
);
|
||||||
|
result.onSuccess((_) async {
|
||||||
|
await getIt<KeyValueStorage>().set(
|
||||||
|
KVKeys.lastOpenedWorkspaceId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
});
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
currentWorkspace: currentWorkspace,
|
currentWorkspace: currentWorkspace,
|
||||||
@ -220,11 +229,21 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||||||
Future<(UserWorkspacePB currentWorkspace, List<UserWorkspacePB> workspaces)?>
|
Future<(UserWorkspacePB currentWorkspace, List<UserWorkspacePB> workspaces)?>
|
||||||
_fetchWorkspaces() async {
|
_fetchWorkspaces() async {
|
||||||
try {
|
try {
|
||||||
|
final lastOpenedWorkspaceId = await getIt<KeyValueStorage>().get(
|
||||||
|
KVKeys.lastOpenedWorkspaceId,
|
||||||
|
);
|
||||||
final currentWorkspace =
|
final currentWorkspace =
|
||||||
await _userService.getCurrentWorkspace().getOrThrow();
|
await _userService.getCurrentWorkspace().getOrThrow();
|
||||||
final workspaces = await _userService.getWorkspaces().getOrThrow();
|
final workspaces = await _userService.getWorkspaces().getOrThrow();
|
||||||
final currentWorkspaceInList =
|
UserWorkspacePB currentWorkspaceInList =
|
||||||
workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id);
|
workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id);
|
||||||
|
if (lastOpenedWorkspaceId != null) {
|
||||||
|
final lastOpenedWorkspace = workspaces
|
||||||
|
.firstWhereOrNull((e) => e.workspaceId == lastOpenedWorkspaceId);
|
||||||
|
if (lastOpenedWorkspace != null) {
|
||||||
|
currentWorkspaceInList = lastOpenedWorkspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
return (currentWorkspaceInList, workspaces);
|
return (currentWorkspaceInList, workspaces);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('fetch workspace error: $e');
|
Log.error('fetch workspace error: $e');
|
||||||
|
@ -67,6 +67,9 @@ class HomeSideBar extends StatelessWidget {
|
|||||||
previous.currentWorkspace?.workspaceId !=
|
previous.currentWorkspace?.workspaceId !=
|
||||||
current.currentWorkspace?.workspaceId,
|
current.currentWorkspace?.workspaceId,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
if (state.currentWorkspace == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
|
@ -181,7 +181,7 @@ class _SidebarSwitchWorkspaceButtonState
|
|||||||
enableEdit: false,
|
enableEdit: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(4),
|
const HSpace(6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
widget.currentWorkspace.name,
|
widget.currentWorkspace.name,
|
||||||
|
@ -44,9 +44,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||||||
width: widget.iconSize,
|
width: widget.iconSize,
|
||||||
height: max(widget.iconSize, 26),
|
height: max(widget.iconSize, 26),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ColorGenerator.generateColorFromString(
|
color: ColorGenerator(widget.workspace.name).toColor(),
|
||||||
widget.workspace.name,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dar
|
|||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SettingsMenu extends StatelessWidget {
|
class SettingsMenu extends StatelessWidget {
|
||||||
@ -79,15 +80,15 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.people,
|
icon: Icons.people,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
// if (kDebugMode)
|
if (kDebugMode)
|
||||||
// SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
// // no need to translate this page
|
// no need to translate this page
|
||||||
// page: SettingsPage.featureFlags,
|
page: SettingsPage.featureFlags,
|
||||||
// selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
// label: 'Feature Flags',
|
label: 'Feature Flags',
|
||||||
// icon: Icons.flag,
|
icon: Icons.flag,
|
||||||
// changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
// ),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
@ -13,6 +11,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class MoreViewActions extends StatefulWidget {
|
class MoreViewActions extends StatefulWidget {
|
||||||
@ -105,8 +104,8 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
|||||||
builder: (context, isHovering) => Padding(
|
builder: (context, isHovering) => Padding(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: FlowySvg(
|
child: FlowySvg(
|
||||||
FlowySvgs.details_s,
|
FlowySvgs.three_dots_vertical_s,
|
||||||
size: const Size(18, 18),
|
size: const Size.square(16),
|
||||||
color: isHovering
|
color: isHovering
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
? Theme.of(context).colorScheme.onPrimary
|
||||||
: Theme.of(context).iconTheme.color,
|
: Theme.of(context).iconTheme.color,
|
||||||
|
@ -29,7 +29,7 @@ class UserAvatar extends StatelessWidget {
|
|||||||
|
|
||||||
if (iconUrl.isEmpty) {
|
if (iconUrl.isEmpty) {
|
||||||
final String nameOrDefault = _userName(name);
|
final String nameOrDefault = _userName(name);
|
||||||
final Color color = ColorGenerator.generateColorFromString(name);
|
final Color color = ColorGenerator(name).toColor();
|
||||||
const initialsCount = 2;
|
const initialsCount = 2;
|
||||||
|
|
||||||
// Taking the first letters of the name components and limiting to 2 elements
|
// Taking the first letters of the name components and limiting to 2 elements
|
||||||
|
@ -53,8 +53,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: a571f2b
|
ref: b927ec0
|
||||||
resolved-ref: a571f2bc9df764d90569951f40364c8c59787f30
|
resolved-ref: b927ec0685c870c731c5b6d9688a031d0cd31e76
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.3.3"
|
version: "2.3.3"
|
||||||
@ -105,6 +105,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
version: "2.11.0"
|
||||||
|
avatar_stack:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: avatar_stack
|
||||||
|
sha256: e4a1576f7478add964bbb8aa5e530db39288fbbf81c30c4fb4b81162dd68aa49
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -19,7 +19,7 @@ version: 0.5.3
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
flutter: ">=3.19.0"
|
flutter: ">=3.19.0"
|
||||||
sdk: ">=3.1.5 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
@ -132,6 +132,7 @@ dependencies:
|
|||||||
share_plus: ^7.2.1
|
share_plus: ^7.2.1
|
||||||
sheet:
|
sheet:
|
||||||
file: ^7.0.0
|
file: ^7.0.0
|
||||||
|
avatar_stack: ^1.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^3.0.1
|
flutter_lints: ^3.0.1
|
||||||
@ -168,7 +169,7 @@ dependency_overrides:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: "a571f2b"
|
ref: "b927ec0"
|
||||||
|
|
||||||
sheet:
|
sheet:
|
||||||
git:
|
git:
|
||||||
|
7
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
7
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -838,7 +838,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -862,7 +861,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -892,7 +890,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -911,7 +908,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-entity"
|
name = "collab-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -926,7 +922,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -964,7 +959,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1041,7 +1035,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-user"
|
name = "collab-user"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
|
@ -96,10 +96,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d4e
|
|||||||
# To switch to the local path, run:
|
# To switch to the local path, run:
|
||||||
# scripts/tool/update_collab_source.sh
|
# scripts/tool/update_collab_source.sh
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
|
@ -65,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d4e
|
|||||||
# To switch to the local path, run:
|
# To switch to the local path, run:
|
||||||
# scripts/tool/update_collab_source.sh
|
# scripts/tool/update_collab_source.sh
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
|
14
frontend/rust-lib/Cargo.lock
generated
14
frontend/rust-lib/Cargo.lock
generated
@ -764,7 +764,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2b42012#2b42012c830682f9bb8314a376c14229804889ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -788,7 +788,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2b42012#2b42012c830682f9bb8314a376c14229804889ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -818,7 +818,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2b42012#2b42012c830682f9bb8314a376c14229804889ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -837,7 +837,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-entity"
|
name = "collab-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2b42012#2b42012c830682f9bb8314a376c14229804889ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -852,7 +852,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2b42012#2b42012c830682f9bb8314a376c14229804889ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -890,7 +890,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2b42012#2b42012c830682f9bb8314a376c14229804889ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -967,7 +967,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-user"
|
name = "collab-user"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2b42012#2b42012c830682f9bb8314a376c14229804889ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
|
@ -120,10 +120,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d4e
|
|||||||
# To switch to the local path, run:
|
# To switch to the local path, run:
|
||||||
# scripts/tool/update_collab_source.sh
|
# scripts/tool/update_collab_source.sh
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||||
|
@ -89,6 +89,14 @@ impl DocumentUserService for DocumentUserImpl {
|
|||||||
.user_id()
|
.user_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_id(&self) -> Result<String, FlowyError> {
|
||||||
|
self
|
||||||
|
.0
|
||||||
|
.upgrade()
|
||||||
|
.ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
|
||||||
|
.device_id()
|
||||||
|
}
|
||||||
|
|
||||||
fn workspace_id(&self) -> Result<String, FlowyError> {
|
fn workspace_id(&self) -> Result<String, FlowyError> {
|
||||||
self
|
self
|
||||||
.0
|
.0
|
||||||
|
@ -10,8 +10,11 @@ use parking_lot::Mutex;
|
|||||||
|
|
||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
use lib_dispatch::prelude::af_spawn;
|
use lib_dispatch::prelude::af_spawn;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::entities::{DocEventPB, DocumentSnapshotStatePB, DocumentSyncStatePB};
|
use crate::entities::{
|
||||||
|
DocEventPB, DocumentAwarenessStatesPB, DocumentSnapshotStatePB, DocumentSyncStatePB,
|
||||||
|
};
|
||||||
use crate::notification::{send_notification, DocumentNotification};
|
use crate::notification::{send_notification, DocumentNotification};
|
||||||
|
|
||||||
/// This struct wrap the document::Document
|
/// This struct wrap the document::Document
|
||||||
@ -50,13 +53,28 @@ impl MutexDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) {
|
fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) {
|
||||||
let doc_id = doc_id.to_string();
|
let doc_id_clone_for_block_changed = doc_id.to_owned();
|
||||||
document
|
document
|
||||||
.lock()
|
.lock()
|
||||||
.subscribe_block_changed(move |events, is_remote| {
|
.subscribe_block_changed(move |events, is_remote| {
|
||||||
|
trace!("subscribe_document_changed: {:?}", events);
|
||||||
// send notification to the client.
|
// send notification to the client.
|
||||||
send_notification(&doc_id, DocumentNotification::DidReceiveUpdate)
|
send_notification(
|
||||||
.payload::<DocEventPB>((events, is_remote).into())
|
&doc_id_clone_for_block_changed,
|
||||||
|
DocumentNotification::DidReceiveUpdate,
|
||||||
|
)
|
||||||
|
.payload::<DocEventPB>((events, is_remote, None).into())
|
||||||
|
.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
let doc_id_clone_for_awareness_state = doc_id.to_owned();
|
||||||
|
document.lock().subscribe_awareness_state(move |events| {
|
||||||
|
trace!("subscribe_awareness_state: {:?}", events);
|
||||||
|
send_notification(
|
||||||
|
&doc_id_clone_for_awareness_state,
|
||||||
|
DocumentNotification::DidUpdateDocumentAwarenessState,
|
||||||
|
)
|
||||||
|
.payload::<DocumentAwarenessStatesPB>(events.into())
|
||||||
.send();
|
.send();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use collab::core::collab_state::SyncState;
|
use collab::core::collab_state::SyncState;
|
||||||
use collab_document::blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData};
|
use collab_document::{
|
||||||
|
blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData},
|
||||||
|
document_awareness::{
|
||||||
|
DocumentAwarenessPosition, DocumentAwarenessSelection, DocumentAwarenessState,
|
||||||
|
DocumentAwarenessUser,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
use flowy_error::ErrorCode;
|
use flowy_error::ErrorCode;
|
||||||
@ -301,6 +307,9 @@ pub struct DocEventPB {
|
|||||||
|
|
||||||
#[pb(index = 2)]
|
#[pb(index = 2)]
|
||||||
pub is_remote: bool,
|
pub is_remote: bool,
|
||||||
|
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub new_snapshot: Option<DocumentDataPB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
@ -512,3 +521,124 @@ pub struct DocumentSnapshotData {
|
|||||||
pub object_id: String,
|
pub object_id: String,
|
||||||
pub encoded_v1: Vec<u8>,
|
pub encoded_v1: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(ProtoBuf, Debug, Default)]
|
||||||
|
pub struct DocumentAwarenessStatesPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub value: HashMap<String, DocumentAwarenessStatePB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HashMap<u64, DocumentAwarenessState>> for DocumentAwarenessStatesPB {
|
||||||
|
fn from(value: HashMap<u64, DocumentAwarenessState>) -> Self {
|
||||||
|
let value = value
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.into()))
|
||||||
|
.collect();
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ProtoBuf, Debug, Default)]
|
||||||
|
pub struct UpdateDocumentAwarenessStatePB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub document_id: String,
|
||||||
|
#[pb(index = 2, one_of)]
|
||||||
|
pub selection: Option<DocumentAwarenessSelectionPB>,
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub metadata: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ProtoBuf, Debug, Default)]
|
||||||
|
pub struct DocumentAwarenessStatePB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub version: i64,
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub user: DocumentAwarenessUserPB,
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub selection: Option<DocumentAwarenessSelectionPB>,
|
||||||
|
#[pb(index = 4, one_of)]
|
||||||
|
pub metadata: Option<String>,
|
||||||
|
#[pb(index = 5)]
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DocumentAwarenessState> for DocumentAwarenessStatePB {
|
||||||
|
fn from(value: DocumentAwarenessState) -> Self {
|
||||||
|
DocumentAwarenessStatePB {
|
||||||
|
version: value.version,
|
||||||
|
user: value.user.into(),
|
||||||
|
selection: value.selection.map(|s| s.into()),
|
||||||
|
metadata: value.metadata,
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ProtoBuf, Debug, Default)]
|
||||||
|
pub struct DocumentAwarenessUserPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub uid: i64,
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub device_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DocumentAwarenessUser> for DocumentAwarenessUserPB {
|
||||||
|
fn from(value: DocumentAwarenessUser) -> Self {
|
||||||
|
DocumentAwarenessUserPB {
|
||||||
|
uid: value.uid,
|
||||||
|
device_id: value.device_id.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ProtoBuf, Debug, Default)]
|
||||||
|
pub struct DocumentAwarenessSelectionPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub start: DocumentAwarenessPositionPB,
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub end: DocumentAwarenessPositionPB,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ProtoBuf, Debug, Default)]
|
||||||
|
pub struct DocumentAwarenessPositionPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub path: Vec<u64>,
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DocumentAwarenessSelectionPB> for DocumentAwarenessSelection {
|
||||||
|
fn from(value: DocumentAwarenessSelectionPB) -> Self {
|
||||||
|
DocumentAwarenessSelection {
|
||||||
|
start: value.start.into(),
|
||||||
|
end: value.end.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DocumentAwarenessSelection> for DocumentAwarenessSelectionPB {
|
||||||
|
fn from(value: DocumentAwarenessSelection) -> Self {
|
||||||
|
DocumentAwarenessSelectionPB {
|
||||||
|
start: value.start.into(),
|
||||||
|
end: value.end.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DocumentAwarenessPositionPB> for DocumentAwarenessPosition {
|
||||||
|
fn from(value: DocumentAwarenessPositionPB) -> Self {
|
||||||
|
DocumentAwarenessPosition {
|
||||||
|
path: value.path,
|
||||||
|
offset: value.offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DocumentAwarenessPosition> for DocumentAwarenessPositionPB {
|
||||||
|
fn from(value: DocumentAwarenessPosition) -> Self {
|
||||||
|
DocumentAwarenessPositionPB {
|
||||||
|
path: value.path,
|
||||||
|
offset: value.offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ use std::sync::{Arc, Weak};
|
|||||||
|
|
||||||
use collab_document::blocks::{
|
use collab_document::blocks::{
|
||||||
BlockAction, BlockActionPayload, BlockActionType, BlockEvent, BlockEventPayload, DeltaType,
|
BlockAction, BlockActionPayload, BlockActionType, BlockEvent, BlockEventPayload, DeltaType,
|
||||||
|
DocumentData,
|
||||||
};
|
};
|
||||||
|
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
@ -293,12 +294,15 @@ impl From<DeltaType> for DeltaTypePB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(&Vec<BlockEvent>, bool)> for DocEventPB {
|
impl From<(&Vec<BlockEvent>, bool, Option<DocumentData>)> for DocEventPB {
|
||||||
fn from((events, is_remote): (&Vec<BlockEvent>, bool)) -> Self {
|
fn from(
|
||||||
|
(events, is_remote, new_snapshot): (&Vec<BlockEvent>, bool, Option<DocumentData>),
|
||||||
|
) -> Self {
|
||||||
// Convert each individual `BlockEvent` to a protobuf `BlockEventPB`, and collect the results into a `Vec`
|
// Convert each individual `BlockEvent` to a protobuf `BlockEventPB`, and collect the results into a `Vec`
|
||||||
Self {
|
Self {
|
||||||
events: events.iter().map(|e| e.to_owned().into()).collect(),
|
events: events.iter().map(|e| e.to_owned().into()).collect(),
|
||||||
is_remote,
|
is_remote,
|
||||||
|
new_snapshot: new_snapshot.map(|d| d.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -451,3 +455,16 @@ pub(crate) async fn delete_file_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
manager.delete_file(local_file_path, url).await
|
manager.delete_file(local_file_path, url).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn set_awareness_local_state_handler(
|
||||||
|
data: AFPluginData<UpdateDocumentAwarenessStatePB>,
|
||||||
|
manager: AFPluginState<Weak<DocumentManager>>,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let manager = upgrade_document(manager)?;
|
||||||
|
let data = data.into_inner();
|
||||||
|
let doc_id = data.document_id.clone();
|
||||||
|
manager
|
||||||
|
.set_document_awareness_local_state(&doc_id, data)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ use strum_macros::Display;
|
|||||||
|
|
||||||
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
||||||
use lib_dispatch::prelude::AFPlugin;
|
use lib_dispatch::prelude::AFPlugin;
|
||||||
|
use tracing::event;
|
||||||
|
|
||||||
use crate::event_handler::get_snapshot_meta_handler;
|
use crate::event_handler::get_snapshot_meta_handler;
|
||||||
use crate::{event_handler::*, manager::DocumentManager};
|
use crate::{event_handler::*, manager::DocumentManager};
|
||||||
@ -42,6 +43,10 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin {
|
|||||||
.event(DocumentEvent::UploadFile, upload_file_handler)
|
.event(DocumentEvent::UploadFile, upload_file_handler)
|
||||||
.event(DocumentEvent::DownloadFile, download_file_handler)
|
.event(DocumentEvent::DownloadFile, download_file_handler)
|
||||||
.event(DocumentEvent::DeleteFile, delete_file_handler)
|
.event(DocumentEvent::DeleteFile, delete_file_handler)
|
||||||
|
.event(
|
||||||
|
DocumentEvent::SetAwarenessState,
|
||||||
|
set_awareness_local_state_handler,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
||||||
@ -118,4 +123,7 @@ pub enum DocumentEvent {
|
|||||||
DownloadFile = 16,
|
DownloadFile = 16,
|
||||||
#[event(input = "UploadedFilePB")]
|
#[event(input = "UploadedFilePB")]
|
||||||
DeleteFile = 17,
|
DeleteFile = 17,
|
||||||
|
|
||||||
|
#[event(input = "UpdateDocumentAwarenessStatePB")]
|
||||||
|
SetAwarenessState = 18,
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,13 @@ use collab::core::origin::CollabOrigin;
|
|||||||
use collab::preclude::Collab;
|
use collab::preclude::Collab;
|
||||||
use collab_document::blocks::DocumentData;
|
use collab_document::blocks::DocumentData;
|
||||||
use collab_document::document::Document;
|
use collab_document::document::Document;
|
||||||
|
use collab_document::document_awareness::DocumentAwarenessState;
|
||||||
|
use collab_document::document_awareness::DocumentAwarenessUser;
|
||||||
use collab_document::document_data::default_document_data;
|
use collab_document::document_data::default_document_data;
|
||||||
use collab_entity::CollabType;
|
use collab_entity::CollabType;
|
||||||
use collab_plugins::CollabKVDB;
|
use collab_plugins::CollabKVDB;
|
||||||
use flowy_storage::object_from_disk;
|
use flowy_storage::object_from_disk;
|
||||||
|
use lib_infra::util::timestamp;
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
@ -26,6 +29,7 @@ use flowy_storage::ObjectStorageService;
|
|||||||
use lib_dispatch::prelude::af_spawn;
|
use lib_dispatch::prelude::af_spawn;
|
||||||
|
|
||||||
use crate::document::MutexDocument;
|
use crate::document::MutexDocument;
|
||||||
|
use crate::entities::UpdateDocumentAwarenessStatePB;
|
||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
DocumentSnapshotData, DocumentSnapshotMeta, DocumentSnapshotMetaPB, DocumentSnapshotPB,
|
DocumentSnapshotData, DocumentSnapshotMeta, DocumentSnapshotMetaPB, DocumentSnapshotPB,
|
||||||
};
|
};
|
||||||
@ -33,6 +37,7 @@ use crate::reminder::DocumentReminderAction;
|
|||||||
|
|
||||||
pub trait DocumentUserService: Send + Sync {
|
pub trait DocumentUserService: Send + Sync {
|
||||||
fn user_id(&self) -> Result<i64, FlowyError>;
|
fn user_id(&self) -> Result<i64, FlowyError>;
|
||||||
|
fn device_id(&self) -> Result<String, FlowyError>;
|
||||||
fn workspace_id(&self) -> Result<String, FlowyError>;
|
fn workspace_id(&self) -> Result<String, FlowyError>;
|
||||||
fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>;
|
fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>;
|
||||||
}
|
}
|
||||||
@ -204,6 +209,8 @@ impl DocumentManager {
|
|||||||
if let Ok(doc) = self.get_document(doc_id).await {
|
if let Ok(doc) = self.get_document(doc_id).await {
|
||||||
trace!("close document: {}", doc_id);
|
trace!("close document: {}", doc_id);
|
||||||
if let Some(doc) = doc.try_lock() {
|
if let Some(doc) = doc.try_lock() {
|
||||||
|
// clear the awareness state when close the document
|
||||||
|
doc.clean_awareness_local_state();
|
||||||
let _ = doc.flush();
|
let _ = doc.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,6 +229,31 @@ impl DocumentManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_document_awareness_local_state(
|
||||||
|
&self,
|
||||||
|
doc_id: &str,
|
||||||
|
state: UpdateDocumentAwarenessStatePB,
|
||||||
|
) -> FlowyResult<bool> {
|
||||||
|
let uid = self.user_service.user_id()?;
|
||||||
|
let device_id = self.user_service.device_id()?;
|
||||||
|
if let Ok(doc) = self.get_document(doc_id).await {
|
||||||
|
if let Some(doc) = doc.try_lock() {
|
||||||
|
let user = DocumentAwarenessUser { uid, device_id };
|
||||||
|
let selection = state.selection.map(|s| s.into());
|
||||||
|
let state = DocumentAwarenessState {
|
||||||
|
version: 1,
|
||||||
|
user,
|
||||||
|
selection,
|
||||||
|
metadata: state.metadata,
|
||||||
|
timestamp: timestamp(),
|
||||||
|
};
|
||||||
|
doc.set_awareness_local_state(state);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the list of snapshots of the document.
|
/// Return the list of snapshots of the document.
|
||||||
pub async fn get_document_snapshot_meta(
|
pub async fn get_document_snapshot_meta(
|
||||||
&self,
|
&self,
|
||||||
|
@ -11,6 +11,7 @@ pub enum DocumentNotification {
|
|||||||
DidReceiveUpdate = 1,
|
DidReceiveUpdate = 1,
|
||||||
DidUpdateDocumentSnapshotState = 2,
|
DidUpdateDocumentSnapshotState = 2,
|
||||||
DidUpdateDocumentSyncState = 3,
|
DidUpdateDocumentSyncState = 3,
|
||||||
|
DidUpdateDocumentAwarenessState = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<DocumentNotification> for i32 {
|
impl std::convert::From<DocumentNotification> for i32 {
|
||||||
@ -24,6 +25,7 @@ impl std::convert::From<i32> for DocumentNotification {
|
|||||||
1 => DocumentNotification::DidReceiveUpdate,
|
1 => DocumentNotification::DidReceiveUpdate,
|
||||||
2 => DocumentNotification::DidUpdateDocumentSnapshotState,
|
2 => DocumentNotification::DidUpdateDocumentSnapshotState,
|
||||||
3 => DocumentNotification::DidUpdateDocumentSyncState,
|
3 => DocumentNotification::DidUpdateDocumentSyncState,
|
||||||
|
4 => DocumentNotification::DidUpdateDocumentAwarenessState,
|
||||||
_ => DocumentNotification::Unknown,
|
_ => DocumentNotification::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,10 @@ impl DocumentUserService for FakeUser {
|
|||||||
fn collab_db(&self, _uid: i64) -> Result<std::sync::Weak<CollabKVDB>, FlowyError> {
|
fn collab_db(&self, _uid: i64) -> Result<std::sync::Weak<CollabKVDB>, FlowyError> {
|
||||||
Ok(Arc::downgrade(&self.collab_db))
|
Ok(Arc::downgrade(&self.collab_db))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_id(&self) -> Result<String, FlowyError> {
|
||||||
|
Ok("".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_log() {
|
pub fn setup_log() {
|
||||||
|
@ -56,6 +56,10 @@ impl AuthenticateUser {
|
|||||||
Ok(session.user_id)
|
Ok(session.user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn device_id(&self) -> FlowyResult<String> {
|
||||||
|
Ok(self.user_config.device_id.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn workspace_id(&self) -> FlowyResult<String> {
|
pub fn workspace_id(&self) -> FlowyResult<String> {
|
||||||
let session = self.get_session()?;
|
let session = self.get_session()?;
|
||||||
Ok(session.user_workspace.id)
|
Ok(session.user_workspace.id)
|
||||||
|
Loading…
Reference in New Issue
Block a user