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:
|
||||
/// {'feature_flag_1': true, 'feature_flag_2': false}
|
||||
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/widgets/flowy_mobile_state_container.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/editor_notification.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
@ -74,6 +75,14 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
viewPB = view;
|
||||
actions.addAll([
|
||||
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),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
|
@ -94,6 +94,9 @@ class MobileHomePage extends StatelessWidget {
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
builder: (context, state) {
|
||||
if (state.currentWorkspace == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
// Header
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.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/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MobileRecentFolder extends StatefulWidget {
|
||||
const MobileRecentFolder({super.key});
|
||||
@ -76,11 +80,70 @@ class _RecentViews extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys.sideBar_recent.tr(),
|
||||
fontSize: 20.0,
|
||||
child: GestureDetector(
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys.sideBar_recent.tr(),
|
||||
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(
|
||||
key: const PageStorageKey('recent_views_page_storage_key'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
|
@ -2,10 +2,10 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy/mobile/application/mobile_router.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/presentation/editor_plugins/plugins.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/view_ext.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
@ -53,7 +53,7 @@ class _MobileRecentViewState extends State<MobileRecentView> {
|
||||
|
||||
documentListener = DocumentListener(id: view.id)
|
||||
..start(
|
||||
didReceiveUpdate: (document) {
|
||||
onDocEventUpdate: (document) {
|
||||
setState(() {
|
||||
view = view;
|
||||
});
|
||||
|
@ -47,7 +47,7 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_version.tr(),
|
||||
trailing: FlowyText(
|
||||
'${DeviceOrApplicationInfoTask.applicationVersion} (${DeviceOrApplicationInfoTask.buildNumber})',
|
||||
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
|
||||
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:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class SelfHostUrlBottomSheet extends StatefulWidget {
|
||||
const SelfHostUrlBottomSheet({
|
||||
@ -38,32 +37,9 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(
|
||||
key: _formKey,
|
||||
child: TextFormField(
|
||||
|
@ -36,6 +36,14 @@ class _SelfHostSettingGroupState extends State<SelfHostSettingGroup> {
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.editor_urlHint.tr(),
|
||||
showCloseButton: true,
|
||||
showDivider: false,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
builder: (_) {
|
||||
return SelfHostUrlBottomSheet(
|
||||
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: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_sync_state_listener.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/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/shared/feature_flags.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/workspace/application/doc/doc_listener.dart';
|
||||
import 'package:appflowy/workspace/application/doc/sync_state_listener.dart';
|
||||
import 'package:appflowy/util/color_generator/color_generator.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_backend/protobuf/flowy-document/entities.pb.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 TrashService _trashService = TrashService();
|
||||
|
||||
late CollabDocumentAdapter _collabDocumentAdapter;
|
||||
late DocumentCollabAdapter _documentCollabAdapter;
|
||||
|
||||
late final TransactionAdapter _transactionAdapter = TransactionAdapter(
|
||||
documentId: view.id,
|
||||
documentService: _documentService,
|
||||
);
|
||||
|
||||
StreamSubscription? _subscription;
|
||||
StreamSubscription? _transactionSubscription;
|
||||
|
||||
final _updateSelectionDebounce = Debounce();
|
||||
final _syncDocDebounce = Debounce();
|
||||
|
||||
bool get isLocalMode {
|
||||
final userProfilePB = state.userProfilePB;
|
||||
@ -70,7 +79,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
await _documentListener.stop();
|
||||
await _syncStateListener.stop();
|
||||
await _viewListener.stop();
|
||||
await _subscription?.cancel();
|
||||
await _transactionSubscription?.cancel();
|
||||
await _documentService.closeDocument(view: view);
|
||||
state.editorState?.service.keyboardService?.closeKeyboard();
|
||||
state.editorState?.dispose();
|
||||
@ -104,6 +113,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
),
|
||||
);
|
||||
emit(newState);
|
||||
if (newState.userProfilePB != null) {
|
||||
await _updateCollaborator();
|
||||
}
|
||||
},
|
||||
moveToTrash: () async {
|
||||
emit(state.copyWith(isDeleted: true));
|
||||
@ -143,7 +155,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
/// subscribe to the document content change
|
||||
void _onDocumentChanged() {
|
||||
_documentListener.start(
|
||||
didReceiveUpdate: syncDocumentDataPB,
|
||||
onDocEventUpdate: _debounceSyncDoc,
|
||||
onDocAwarenessUpdate: _onAwarenessStatesUpdate,
|
||||
);
|
||||
|
||||
_syncStateListener.start(
|
||||
@ -173,24 +186,31 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
_collabDocumentAdapter = CollabDocumentAdapter(editorState, view.id);
|
||||
_documentCollabAdapter = DocumentCollabAdapter(editorState, view.id);
|
||||
|
||||
// subscribe to the document change from the editor
|
||||
_subscription = editorState.transactionStream.listen((event) async {
|
||||
final time = event.$1;
|
||||
if (time != TransactionTime.before) {
|
||||
return;
|
||||
}
|
||||
await _transactionAdapter.apply(event.$2, editorState);
|
||||
_transactionSubscription = editorState.transactionStream.listen(
|
||||
(event) async {
|
||||
final time = event.$1;
|
||||
final transaction = event.$2;
|
||||
if (time != TransactionTime.before) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the document is empty.
|
||||
await applyRules();
|
||||
// apply transaction to backend
|
||||
await _transactionAdapter.apply(transaction, editorState);
|
||||
|
||||
if (!isClosed) {
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty));
|
||||
}
|
||||
});
|
||||
// check if the document is empty.
|
||||
await _applyRules();
|
||||
|
||||
if (!isClosed) {
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
editorState.selectionNotifier.addListener(_debounceOnSelectionUpdate);
|
||||
|
||||
// output the log from the editor when debug mode
|
||||
if (kDebugMode) {
|
||||
@ -204,14 +224,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
return editorState;
|
||||
}
|
||||
|
||||
Future<void> applyRules() async {
|
||||
Future<void> _applyRules() async {
|
||||
await Future.wait([
|
||||
ensureAtLeastOneParagraphExists(),
|
||||
ensureLastNodeIsEditable(),
|
||||
_ensureAtLeastOneParagraphExists(),
|
||||
_ensureLastNodeIsEditable(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> ensureLastNodeIsEditable() async {
|
||||
Future<void> _ensureLastNodeIsEditable() async {
|
||||
final editorState = state.editorState;
|
||||
if (editorState == null) {
|
||||
return;
|
||||
@ -226,7 +246,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> ensureAtLeastOneParagraphExists() async {
|
||||
Future<void> _ensureAtLeastOneParagraphExists() async {
|
||||
final editorState = state.editorState;
|
||||
if (editorState == null) {
|
||||
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) {
|
||||
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,
|
||||
EditorState? editorState,
|
||||
FlowyError? error,
|
||||
@Default(null) DocumentAwarenessStatesPB? awarenessStates,
|
||||
}) = _DocumentState;
|
||||
|
||||
factory DocumentState.initial() => const DocumentState(
|
||||
|
@ -1,15 +1,21 @@
|
||||
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/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_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
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 {
|
||||
CollabDocumentAdapter(this.editorState, this.docId);
|
||||
class DocumentCollabAdapter {
|
||||
DocumentCollabAdapter(this.editorState, this.docId);
|
||||
|
||||
final EditorState editorState;
|
||||
final String docId;
|
||||
@ -61,7 +67,7 @@ class CollabDocumentAdapter {
|
||||
/// Sync version 3
|
||||
///
|
||||
/// 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 document = result.fold((s) => s.toDocument(), (f) => null);
|
||||
if (document == null) {
|
||||
@ -70,9 +76,12 @@ class CollabDocumentAdapter {
|
||||
|
||||
final ops = diffNodes(editorState.document.root, document.root);
|
||||
if (ops.isEmpty) {
|
||||
debugPrint('[collab] received empty ops');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[collab] received ops: $ops');
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
for (final op in ops) {
|
||||
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> {
|
@ -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_result/appflowy_result.dart';
|
||||
|
||||
typedef OnDocumentEventUpdate = void Function(DocEventPB docEvent);
|
||||
typedef OnDocumentAwarenessStateUpdate = void Function(
|
||||
DocumentAwarenessStatesPB awarenessStates,
|
||||
);
|
||||
|
||||
class DocumentListener {
|
||||
DocumentListener({
|
||||
required this.id,
|
||||
@ -18,12 +23,15 @@ class DocumentListener {
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
DocumentNotificationParser? _parser;
|
||||
|
||||
Function(DocEventPB docEvent)? didReceiveUpdate;
|
||||
OnDocumentEventUpdate? _onDocEventUpdate;
|
||||
OnDocumentAwarenessStateUpdate? _onDocAwarenessUpdate;
|
||||
|
||||
void start({
|
||||
Function(DocEventPB docEvent)? didReceiveUpdate,
|
||||
OnDocumentEventUpdate? onDocEventUpdate,
|
||||
OnDocumentAwarenessStateUpdate? onDocAwarenessUpdate,
|
||||
}) {
|
||||
this.didReceiveUpdate = didReceiveUpdate;
|
||||
_onDocEventUpdate = onDocEventUpdate;
|
||||
_onDocAwarenessUpdate = onDocAwarenessUpdate;
|
||||
|
||||
_parser = DocumentNotificationParser(
|
||||
id: id,
|
||||
@ -40,7 +48,16 @@ class DocumentListener {
|
||||
) {
|
||||
switch (ty) {
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
@ -48,6 +65,8 @@ class DocumentListener {
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
_onDocAwarenessUpdate = null;
|
||||
_onDocEventUpdate = null;
|
||||
await _subscription?.cancel();
|
||||
_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-error/errors.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:fixnum/fixnum.dart';
|
||||
|
||||
class DocumentService {
|
||||
// unused now.
|
||||
@ -143,4 +145,39 @@ class DocumentService {
|
||||
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 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart';
|
||||
import 'package:appflowy/startup/startup.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/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@ -49,12 +49,13 @@ class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
|
||||
|
||||
connectivityStream =
|
||||
_connectivity.onConnectivityChanged.listen((result) {
|
||||
if (!isClosed) {}
|
||||
emit(
|
||||
state.copyWith(
|
||||
isNetworkConnected: result != ConnectivityResult.none,
|
||||
),
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isNetworkConnected: result != ConnectivityResult.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
syncStateChanged: (syncState) {
|
||||
|
@ -144,10 +144,11 @@ extension BlockToNode on BlockPB {
|
||||
final deltaString = meta.textMap[externalId];
|
||||
if (deltaString != null) {
|
||||
final delta = jsonDecode(deltaString);
|
||||
map.putIfAbsent(
|
||||
'delta',
|
||||
() => delta,
|
||||
);
|
||||
map['delta'] = delta;
|
||||
// map.putIfAbsent(
|
||||
// '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/plugins/document/application/document_appearance_cubit.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/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
@ -140,20 +141,27 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DocumentShareButton(
|
||||
key: ValueKey('share_button_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
...FeatureFlag.syncDocument.isOn
|
||||
? [
|
||||
const HSpace(20),
|
||||
DocumentCollaborators(
|
||||
key: ValueKey('collaborators_${view.id}'),
|
||||
width: 100,
|
||||
height: 32,
|
||||
view: view,
|
||||
),
|
||||
const HSpace(16),
|
||||
DocumentSyncIndicator(
|
||||
key: ValueKey('sync_state_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
const HSpace(12),
|
||||
const HSpace(16),
|
||||
]
|
||||
: [const HSpace(8)],
|
||||
DocumentShareButton(
|
||||
key: ValueKey('share_button_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
const HSpace(4),
|
||||
ViewFavoriteButton(
|
||||
key: ValueKey('favorite_button_${view.id}'),
|
||||
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) {
|
||||
// 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) {
|
||||
return;
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ enum FeatureFlag {
|
||||
case FeatureFlag.membersSettings:
|
||||
return false;
|
||||
case FeatureFlag.syncDocument:
|
||||
return false;
|
||||
return true;
|
||||
case FeatureFlag.unknown:
|
||||
return false;
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ class FlowyRunner {
|
||||
if (!mode.isUnitTest) ...[
|
||||
// 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.
|
||||
const DeviceOrApplicationInfoTask(),
|
||||
const ApplicationInfoTask(),
|
||||
const HotKeyTask(),
|
||||
if (isSupabaseEnabled) InitSupabaseTask(),
|
||||
if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(),
|
||||
|
@ -1,16 +1,20 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../startup.dart';
|
||||
|
||||
class DeviceOrApplicationInfoTask extends LaunchTask {
|
||||
const DeviceOrApplicationInfoTask();
|
||||
|
||||
class ApplicationInfo {
|
||||
static int androidSDKVersion = -1;
|
||||
static String applicationVersion = '';
|
||||
static String buildNumber = '';
|
||||
static String deviceId = '';
|
||||
}
|
||||
|
||||
class ApplicationInfoTask extends LaunchTask {
|
||||
const ApplicationInfoTask();
|
||||
|
||||
@override
|
||||
Future<void> initialize(LaunchContext context) async {
|
||||
@ -19,13 +23,41 @@ class DeviceOrApplicationInfoTask extends LaunchTask {
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
androidSDKVersion = androidInfo.version.sdkInt;
|
||||
ApplicationInfo.androidSDKVersion = androidInfo.version.sdkInt;
|
||||
}
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
applicationVersion = packageInfo.version;
|
||||
buildNumber = packageInfo.buildNumber;
|
||||
ApplicationInfo.applicationVersion = packageInfo.version;
|
||||
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
|
||||
|
@ -25,7 +25,7 @@ Future<String> getDeviceId() async {
|
||||
deviceId = macInfo.systemGUID;
|
||||
} else if (Platform.isWindows) {
|
||||
final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo;
|
||||
deviceId = windowsInfo.computerName;
|
||||
deviceId = windowsInfo.deviceId;
|
||||
} else if (Platform.isLinux) {
|
||||
final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;
|
||||
deviceId = linuxInfo.machineId;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorGenerator {
|
||||
static Color generateColorFromString(String string) {
|
||||
final int hash =
|
||||
string.codeUnits.fold(0, (int acc, int unit) => acc + unit);
|
||||
extension type ColorGenerator(String value) {
|
||||
Color toColor() {
|
||||
final int hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit);
|
||||
final double hue = (hash % 360).toDouble();
|
||||
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';
|
||||
|
||||
extension ColorExtensionn on Color {
|
||||
extension ColorExtension on Color {
|
||||
/// return a hex string in 0xff000000 format
|
||||
String toHexString() {
|
||||
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/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
@ -135,6 +138,12 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
),
|
||||
(e) => state.currentWorkspace,
|
||||
);
|
||||
result.onSuccess((_) async {
|
||||
await getIt<KeyValueStorage>().set(
|
||||
KVKeys.lastOpenedWorkspaceId,
|
||||
workspaceId,
|
||||
);
|
||||
});
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentWorkspace: currentWorkspace,
|
||||
@ -220,11 +229,21 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
Future<(UserWorkspacePB currentWorkspace, List<UserWorkspacePB> workspaces)?>
|
||||
_fetchWorkspaces() async {
|
||||
try {
|
||||
final lastOpenedWorkspaceId = await getIt<KeyValueStorage>().get(
|
||||
KVKeys.lastOpenedWorkspaceId,
|
||||
);
|
||||
final currentWorkspace =
|
||||
await _userService.getCurrentWorkspace().getOrThrow();
|
||||
final workspaces = await _userService.getWorkspaces().getOrThrow();
|
||||
final currentWorkspaceInList =
|
||||
UserWorkspacePB currentWorkspaceInList =
|
||||
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);
|
||||
} catch (e) {
|
||||
Log.error('fetch workspace error: $e');
|
||||
|
@ -67,6 +67,9 @@ class HomeSideBar extends StatelessWidget {
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
builder: (context, state) {
|
||||
if (state.currentWorkspace == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
|
@ -181,7 +181,7 @@ class _SidebarSwitchWorkspaceButtonState
|
||||
enableEdit: false,
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
const HSpace(6),
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
widget.currentWorkspace.name,
|
||||
|
@ -44,9 +44,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
||||
width: widget.iconSize,
|
||||
height: max(widget.iconSize, 26),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorGenerator.generateColorFromString(
|
||||
widget.workspace.name,
|
||||
),
|
||||
color: ColorGenerator(widget.workspace.name).toColor(),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsMenu extends StatelessWidget {
|
||||
@ -79,15 +80,15 @@ class SettingsMenu extends StatelessWidget {
|
||||
icon: Icons.people,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
// if (kDebugMode)
|
||||
// SettingsMenuElement(
|
||||
// // no need to translate this page
|
||||
// page: SettingsPage.featureFlags,
|
||||
// selectedPage: currentPage,
|
||||
// label: 'Feature Flags',
|
||||
// icon: Icons.flag,
|
||||
// changeSelectedPage: changeSelectedPage,
|
||||
// ),
|
||||
if (kDebugMode)
|
||||
SettingsMenuElement(
|
||||
// no need to translate this page
|
||||
page: SettingsPage.featureFlags,
|
||||
selectedPage: currentPage,
|
||||
label: 'Feature Flags',
|
||||
icon: Icons.flag,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.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/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MoreViewActions extends StatefulWidget {
|
||||
@ -105,8 +104,8 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
||||
builder: (context, isHovering) => Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
size: const Size(18, 18),
|
||||
FlowySvgs.three_dots_vertical_s,
|
||||
size: const Size.square(16),
|
||||
color: isHovering
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).iconTheme.color,
|
||||
|
@ -29,7 +29,7 @@ class UserAvatar extends StatelessWidget {
|
||||
|
||||
if (iconUrl.isEmpty) {
|
||||
final String nameOrDefault = _userName(name);
|
||||
final Color color = ColorGenerator.generateColorFromString(name);
|
||||
final Color color = ColorGenerator(name).toColor();
|
||||
const initialsCount = 2;
|
||||
|
||||
// Taking the first letters of the name components and limiting to 2 elements
|
||||
|
@ -53,8 +53,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: a571f2b
|
||||
resolved-ref: a571f2bc9df764d90569951f40364c8c59787f30
|
||||
ref: b927ec0
|
||||
resolved-ref: b927ec0685c870c731c5b6d9688a031d0cd31e76
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "2.3.3"
|
||||
@ -105,6 +105,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -19,7 +19,7 @@ version: 0.5.3
|
||||
|
||||
environment:
|
||||
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.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@ -132,6 +132,7 @@ dependencies:
|
||||
share_plus: ^7.2.1
|
||||
sheet:
|
||||
file: ^7.0.0
|
||||
avatar_stack: ^1.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^3.0.1
|
||||
@ -168,7 +169,7 @@ dependency_overrides:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "a571f2b"
|
||||
ref: "b927ec0"
|
||||
|
||||
sheet:
|
||||
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]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -862,7 +861,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -892,7 +890,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -911,7 +908,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -926,7 +922,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -964,7 +959,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1041,7 +1035,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-user"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=22ed64d598cd060a7b68554af61df3568e39a62a#22ed64d598cd060a7b68554af61df3568e39a62a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
|
@ -96,10 +96,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d4e
|
||||
# To switch to the local path, run:
|
||||
# scripts/tool/update_collab_source.sh
|
||||
# ⚠️⚠️⚠️️
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-entity = { 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 = "2b42012" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
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:
|
||||
# scripts/tool/update_collab_source.sh
|
||||
# ⚠️⚠️⚠️️
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-entity = { 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 = "2b42012" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
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]]
|
||||
name = "collab"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -788,7 +788,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -818,7 +818,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -837,7 +837,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -852,7 +852,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -890,7 +890,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -967,7 +967,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-user"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
|
@ -120,10 +120,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d4e
|
||||
# To switch to the local path, run:
|
||||
# scripts/tool/update_collab_source.sh
|
||||
# ⚠️⚠️⚠️️
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "22ed64d598cd060a7b68554af61df3568e39a62a" }
|
||||
collab-entity = { 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 = "2b42012" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2b42012" }
|
||||
|
@ -89,6 +89,14 @@ impl DocumentUserService for DocumentUserImpl {
|
||||
.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> {
|
||||
self
|
||||
.0
|
||||
|
@ -10,8 +10,11 @@ use parking_lot::Mutex;
|
||||
|
||||
use flowy_error::FlowyResult;
|
||||
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};
|
||||
|
||||
/// This struct wrap the document::Document
|
||||
@ -50,15 +53,30 @@ impl 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
|
||||
.lock()
|
||||
.subscribe_block_changed(move |events, is_remote| {
|
||||
trace!("subscribe_document_changed: {:?}", events);
|
||||
// send notification to the client.
|
||||
send_notification(&doc_id, DocumentNotification::DidReceiveUpdate)
|
||||
.payload::<DocEventPB>((events, is_remote).into())
|
||||
.send();
|
||||
send_notification(
|
||||
&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();
|
||||
});
|
||||
}
|
||||
|
||||
fn subscribe_document_snapshot_state(collab: &Arc<MutexCollab>) {
|
||||
|
@ -1,7 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
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_error::ErrorCode;
|
||||
@ -301,6 +307,9 @@ pub struct DocEventPB {
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub is_remote: bool,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub new_snapshot: Option<DocumentDataPB>,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
@ -512,3 +521,124 @@ pub struct DocumentSnapshotData {
|
||||
pub object_id: String,
|
||||
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::{
|
||||
BlockAction, BlockActionPayload, BlockActionType, BlockEvent, BlockEventPayload, DeltaType,
|
||||
DocumentData,
|
||||
};
|
||||
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
@ -293,12 +294,15 @@ impl From<DeltaType> for DeltaTypePB {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&Vec<BlockEvent>, bool)> for DocEventPB {
|
||||
fn from((events, is_remote): (&Vec<BlockEvent>, bool)) -> Self {
|
||||
impl From<(&Vec<BlockEvent>, bool, Option<DocumentData>)> for DocEventPB {
|
||||
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`
|
||||
Self {
|
||||
events: events.iter().map(|e| e.to_owned().into()).collect(),
|
||||
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)?;
|
||||
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 lib_dispatch::prelude::AFPlugin;
|
||||
use tracing::event;
|
||||
|
||||
use crate::event_handler::get_snapshot_meta_handler;
|
||||
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::DownloadFile, download_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)]
|
||||
@ -118,4 +123,7 @@ pub enum DocumentEvent {
|
||||
DownloadFile = 16,
|
||||
#[event(input = "UploadedFilePB")]
|
||||
DeleteFile = 17,
|
||||
|
||||
#[event(input = "UpdateDocumentAwarenessStatePB")]
|
||||
SetAwarenessState = 18,
|
||||
}
|
||||
|
@ -8,10 +8,13 @@ use collab::core::origin::CollabOrigin;
|
||||
use collab::preclude::Collab;
|
||||
use collab_document::blocks::DocumentData;
|
||||
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_entity::CollabType;
|
||||
use collab_plugins::CollabKVDB;
|
||||
use flowy_storage::object_from_disk;
|
||||
use lib_infra::util::timestamp;
|
||||
use lru::LruCache;
|
||||
use parking_lot::Mutex;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@ -26,6 +29,7 @@ use flowy_storage::ObjectStorageService;
|
||||
use lib_dispatch::prelude::af_spawn;
|
||||
|
||||
use crate::document::MutexDocument;
|
||||
use crate::entities::UpdateDocumentAwarenessStatePB;
|
||||
use crate::entities::{
|
||||
DocumentSnapshotData, DocumentSnapshotMeta, DocumentSnapshotMetaPB, DocumentSnapshotPB,
|
||||
};
|
||||
@ -33,6 +37,7 @@ use crate::reminder::DocumentReminderAction;
|
||||
|
||||
pub trait DocumentUserService: Send + Sync {
|
||||
fn user_id(&self) -> Result<i64, FlowyError>;
|
||||
fn device_id(&self) -> Result<String, FlowyError>;
|
||||
fn workspace_id(&self) -> Result<String, 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 {
|
||||
trace!("close document: {}", doc_id);
|
||||
if let Some(doc) = doc.try_lock() {
|
||||
// clear the awareness state when close the document
|
||||
doc.clean_awareness_local_state();
|
||||
let _ = doc.flush();
|
||||
}
|
||||
}
|
||||
@ -222,6 +229,31 @@ impl DocumentManager {
|
||||
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.
|
||||
pub async fn get_document_snapshot_meta(
|
||||
&self,
|
||||
|
@ -11,6 +11,7 @@ pub enum DocumentNotification {
|
||||
DidReceiveUpdate = 1,
|
||||
DidUpdateDocumentSnapshotState = 2,
|
||||
DidUpdateDocumentSyncState = 3,
|
||||
DidUpdateDocumentAwarenessState = 4,
|
||||
}
|
||||
|
||||
impl std::convert::From<DocumentNotification> for i32 {
|
||||
@ -24,6 +25,7 @@ impl std::convert::From<i32> for DocumentNotification {
|
||||
1 => DocumentNotification::DidReceiveUpdate,
|
||||
2 => DocumentNotification::DidUpdateDocumentSnapshotState,
|
||||
3 => DocumentNotification::DidUpdateDocumentSyncState,
|
||||
4 => DocumentNotification::DidUpdateDocumentAwarenessState,
|
||||
_ => DocumentNotification::Unknown,
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,10 @@ impl DocumentUserService for FakeUser {
|
||||
fn collab_db(&self, _uid: i64) -> Result<std::sync::Weak<CollabKVDB>, FlowyError> {
|
||||
Ok(Arc::downgrade(&self.collab_db))
|
||||
}
|
||||
|
||||
fn device_id(&self) -> Result<String, FlowyError> {
|
||||
Ok("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_log() {
|
||||
|
@ -56,6 +56,10 @@ impl AuthenticateUser {
|
||||
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> {
|
||||
let session = self.get_session()?;
|
||||
Ok(session.user_workspace.id)
|
||||
|
Loading…
Reference in New Issue
Block a user