mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: initial version for collab document (#4937)
* feat: initial version for collab document * feat: show sync indicator * feat: add sync document feature flag * fix: rust ci * chore: remove unused code * chore: update doc_bloc.dart
This commit is contained in:
parent
37f521ae57
commit
5f8ef3856a
@ -26,7 +26,7 @@ extension on ViewPB {
|
||||
String get routeName {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Document:
|
||||
return MobileEditorScreen.routeName;
|
||||
return MobileDocumentScreen.routeName;
|
||||
case ViewLayoutPB.Grid:
|
||||
return MobileGridScreen.routeName;
|
||||
case ViewLayoutPB.Calendar:
|
||||
@ -42,8 +42,8 @@ extension on ViewPB {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Document:
|
||||
return {
|
||||
MobileEditorScreen.viewId: id,
|
||||
MobileEditorScreen.viewTitle: name,
|
||||
MobileDocumentScreen.viewId: id,
|
||||
MobileDocumentScreen.viewTitle: name,
|
||||
};
|
||||
case ViewLayoutPB.Grid:
|
||||
return {
|
||||
|
@ -4,7 +4,9 @@ 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_sync_indicator.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
@ -70,7 +72,13 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
} else {
|
||||
body = state.data!.fold((view) {
|
||||
viewPB = view;
|
||||
actions.add(_buildAppBarMoreButton(view));
|
||||
actions.addAll([
|
||||
if (FeatureFlag.syncDocument.isOn) ...[
|
||||
DocumentSyncIndicator(view: view),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
_buildAppBarMoreButton(view),
|
||||
]);
|
||||
final plugin = view.plugin(arguments: widget.arguments ?? const {})
|
||||
..init();
|
||||
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
|
||||
|
@ -2,8 +2,8 @@ import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileEditorScreen extends StatelessWidget {
|
||||
const MobileEditorScreen({
|
||||
class MobileDocumentScreen extends StatelessWidget {
|
||||
const MobileDocumentScreen({
|
||||
super.key,
|
||||
required this.id,
|
||||
this.title,
|
||||
|
@ -0,0 +1,135 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.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';
|
||||
|
||||
class CollabDocumentAdapter {
|
||||
CollabDocumentAdapter(this.editorState, this.docId);
|
||||
|
||||
final EditorState editorState;
|
||||
final String docId;
|
||||
|
||||
final _service = DocumentService();
|
||||
|
||||
/// Sync version 1
|
||||
///
|
||||
/// Force to reload the document
|
||||
///
|
||||
/// Only use in development
|
||||
Future<EditorState?> syncV1() async {
|
||||
final result = await _service.getDocument(viewId: docId);
|
||||
final document = result.fold((s) => s.toDocument(), (f) => null);
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
return EditorState(document: document);
|
||||
}
|
||||
|
||||
/// Sync version 2
|
||||
///
|
||||
/// Translate the [docEvent] from yrs to [Operation]s and apply it to the [editorState]
|
||||
///
|
||||
/// Not fully implemented yet
|
||||
Future<void> syncV2(DocEventPB docEvent) async {
|
||||
prettyPrintJson(docEvent.toProto3Json());
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
for (final event in docEvent.events) {
|
||||
for (final blockEvent in event.event) {
|
||||
switch (blockEvent.command) {
|
||||
case DeltaTypePB.Inserted:
|
||||
break;
|
||||
case DeltaTypePB.Updated:
|
||||
await _syncUpdated(blockEvent, transaction);
|
||||
break;
|
||||
case DeltaTypePB.Removed:
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await editorState.apply(transaction, isRemote: true);
|
||||
}
|
||||
|
||||
/// Sync version 3
|
||||
///
|
||||
/// Diff the local document with the remote document and apply the changes
|
||||
Future<void> syncV3() async {
|
||||
final result = await _service.getDocument(viewId: docId);
|
||||
final document = result.fold((s) => s.toDocument(), (f) => null);
|
||||
if (document == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ops = diffNodes(editorState.document.root, document.root);
|
||||
if (ops.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
for (final op in ops) {
|
||||
transaction.add(op);
|
||||
}
|
||||
await editorState.apply(transaction, isRemote: true);
|
||||
}
|
||||
|
||||
Future<void> _syncUpdated(
|
||||
BlockEventPayloadPB payload,
|
||||
Transaction transaction,
|
||||
) async {
|
||||
assert(payload.command == DeltaTypePB.Updated);
|
||||
|
||||
final path = payload.path;
|
||||
final id = payload.id;
|
||||
final value = jsonDecode(payload.value);
|
||||
|
||||
final nodes = NodeIterator(
|
||||
document: editorState.document,
|
||||
startNode: editorState.document.root,
|
||||
).toList();
|
||||
|
||||
// 1. meta -> text_map = text delta change
|
||||
if (path.isTextDeltaChangeset) {
|
||||
// find the 'text' block and apply the delta
|
||||
// ⚠️ not completed yet.
|
||||
final target = nodes.singleWhereOrNull((n) => n.id == id);
|
||||
if (target != null) {
|
||||
try {
|
||||
final delta = Delta.fromJson(jsonDecode(value));
|
||||
transaction.insertTextDelta(target, 0, delta);
|
||||
} catch (e) {
|
||||
Log.error('Failed to apply delta: $value, error: $e');
|
||||
}
|
||||
}
|
||||
} else if (path.isBlockChangeset) {
|
||||
final target = nodes.singleWhereOrNull((n) => n.id == id);
|
||||
if (target != null) {
|
||||
try {
|
||||
final delta = jsonDecode(value['data'])['delta'];
|
||||
transaction.updateNode(target, {
|
||||
'delta': Delta.fromJson(delta).toJson(),
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error('Failed to update $value, error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on List<String> {
|
||||
bool get isTextDeltaChangeset {
|
||||
return length == 3 && this[0] == 'meta' && this[1] == 'text_map';
|
||||
}
|
||||
|
||||
bool get isBlockChangeset {
|
||||
return length == 2 && this[0] == 'blocks';
|
||||
}
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/collab_document_adapter.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_service.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/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/util/json_print.dart';
|
||||
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
||||
import 'package:appflowy/workspace/application/doc/sync_state_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/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@ -48,6 +50,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final DocumentService _documentService = DocumentService();
|
||||
final TrashService _trashService = TrashService();
|
||||
|
||||
late CollabDocumentAdapter _collabDocumentAdapter;
|
||||
|
||||
late final TransactionAdapter _transactionAdapter = TransactionAdapter(
|
||||
documentId: view.id,
|
||||
documentService: _documentService,
|
||||
@ -79,10 +83,10 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final editorState = await _fetchDocumentState();
|
||||
final result = await _fetchDocumentState();
|
||||
_onViewChanged();
|
||||
_onDocumentChanged();
|
||||
final newState = await editorState.fold(
|
||||
final newState = await result.fold(
|
||||
(s) async {
|
||||
final userProfilePB =
|
||||
await getIt<AuthService>().getUser().toNullable();
|
||||
@ -117,8 +121,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final isDeleted = result.fold((l) => false, (r) => true);
|
||||
emit(state.copyWith(isDeleted: isDeleted));
|
||||
},
|
||||
syncStateChanged: (isSyncing) {
|
||||
emit(state.copyWith(isSyncing: isSyncing));
|
||||
syncStateChanged: (syncState) {
|
||||
emit(state.copyWith(syncState: syncState.value));
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -145,7 +149,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
_syncStateListener.start(
|
||||
didReceiveSyncState: (syncState) {
|
||||
if (!isClosed) {
|
||||
add(DocumentEvent.syncStateChanged(syncState.isSyncing));
|
||||
add(DocumentEvent.syncStateChanged(syncState));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -169,6 +173,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
_collabDocumentAdapter = CollabDocumentAdapter(editorState, view.id);
|
||||
|
||||
// subscribe to the document change from the editor
|
||||
_subscription = editorState.transactionStream.listen((event) async {
|
||||
final time = event.$1;
|
||||
@ -236,21 +242,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
}
|
||||
|
||||
void syncDocumentDataPB(DocEventPB docEvent) {
|
||||
prettyPrintJson(docEvent.toProto3Json());
|
||||
for (final event in docEvent.events) {
|
||||
for (final blockEvent in event.event) {
|
||||
switch (blockEvent.command) {
|
||||
case DeltaTypePB.Inserted:
|
||||
break;
|
||||
case DeltaTypePB.Updated:
|
||||
break;
|
||||
case DeltaTypePB.Removed:
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
Future<void> syncDocumentDataPB(DocEventPB docEvent) async {
|
||||
if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _collabDocumentAdapter.syncV3();
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,17 +258,18 @@ class DocumentEvent with _$DocumentEvent {
|
||||
const factory DocumentEvent.restore() = Restore;
|
||||
const factory DocumentEvent.restorePage() = RestorePage;
|
||||
const factory DocumentEvent.deletePermanently() = DeletePermanently;
|
||||
const factory DocumentEvent.syncStateChanged(bool isSyncing) =
|
||||
syncStateChanged;
|
||||
const factory DocumentEvent.syncStateChanged(
|
||||
final DocumentSyncStatePB syncState,
|
||||
) = syncStateChanged;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentState with _$DocumentState {
|
||||
const factory DocumentState({
|
||||
required bool isDeleted,
|
||||
required bool forceClose,
|
||||
required bool isLoading,
|
||||
required bool isSyncing,
|
||||
required final bool isDeleted,
|
||||
required final bool forceClose,
|
||||
required final bool isLoading,
|
||||
required final DocumentSyncState syncState,
|
||||
bool? isDocumentEmpty,
|
||||
UserProfilePB? userProfilePB,
|
||||
EditorState? editorState,
|
||||
@ -282,6 +280,6 @@ class DocumentState with _$DocumentState {
|
||||
isDeleted: false,
|
||||
forceClose: false,
|
||||
isLoading: true,
|
||||
isSyncing: false,
|
||||
syncState: DocumentSyncState.Syncing,
|
||||
);
|
||||
}
|
||||
|
@ -26,6 +26,14 @@ class DocumentService {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<FlowyResult<DocumentDataPB, FlowyError>> getDocument({
|
||||
required String viewId,
|
||||
}) async {
|
||||
final payload = OpenDocumentPayloadPB()..documentId = viewId;
|
||||
final result = await DocumentEventGetDocumentData(payload).send();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<FlowyResult<BlockPB, FlowyError>> getBlockFromDocument({
|
||||
required DocumentDataPB document,
|
||||
required String blockId,
|
||||
|
@ -0,0 +1,101 @@
|
||||
import 'dart:async';
|
||||
|
||||
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';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'doc_sync_bloc.freezed.dart';
|
||||
|
||||
class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
|
||||
DocumentSyncBloc({
|
||||
required this.view,
|
||||
}) : _syncStateListener = DocumentSyncStateListener(id: view.id),
|
||||
super(DocumentSyncBlocState.initial()) {
|
||||
on<DocumentSyncEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final userProfile = await getIt<AuthService>().getUser().then(
|
||||
(result) => result.fold(
|
||||
(l) => l,
|
||||
(r) => null,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator:
|
||||
userProfile?.authenticator != AuthenticatorPB.Local,
|
||||
),
|
||||
);
|
||||
_syncStateListener.start(
|
||||
didReceiveSyncState: (syncState) {
|
||||
if (!isClosed) {
|
||||
add(DocumentSyncEvent.syncStateChanged(syncState));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final isNetworkConnected = await _connectivity
|
||||
.checkConnectivity()
|
||||
.then((value) => value != ConnectivityResult.none);
|
||||
emit(state.copyWith(isNetworkConnected: isNetworkConnected));
|
||||
|
||||
connectivityStream =
|
||||
_connectivity.onConnectivityChanged.listen((result) {
|
||||
if (!isClosed) {}
|
||||
emit(
|
||||
state.copyWith(
|
||||
isNetworkConnected: result != ConnectivityResult.none,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
syncStateChanged: (syncState) {
|
||||
emit(state.copyWith(syncState: syncState.value));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
final DocumentSyncStateListener _syncStateListener;
|
||||
final _connectivity = Connectivity();
|
||||
|
||||
StreamSubscription? connectivityStream;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await connectivityStream?.cancel();
|
||||
await _syncStateListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentSyncEvent with _$DocumentSyncEvent {
|
||||
const factory DocumentSyncEvent.initial() = Initial;
|
||||
const factory DocumentSyncEvent.syncStateChanged(
|
||||
DocumentSyncStatePB syncState,
|
||||
) = syncStateChanged;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentSyncBlocState with _$DocumentSyncBlocState {
|
||||
const factory DocumentSyncBlocState({
|
||||
required DocumentSyncState syncState,
|
||||
@Default(true) bool isNetworkConnected,
|
||||
@Default(false) bool shouldShowIndicator,
|
||||
}) = _DocumentSyncState;
|
||||
|
||||
factory DocumentSyncBlocState.initial() => const DocumentSyncBlocState(
|
||||
syncState: DocumentSyncState.Syncing,
|
||||
);
|
||||
}
|
@ -101,10 +101,16 @@ extension DocumentDataPBFromTo on DocumentDataPB {
|
||||
children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull());
|
||||
}
|
||||
|
||||
return block?.toNode(
|
||||
final node = block?.toNode(
|
||||
children: children,
|
||||
meta: meta,
|
||||
);
|
||||
|
||||
for (final element in children) {
|
||||
element.parent = node;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
library document_plugin;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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_sync_indicator.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
@ -19,6 +19,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
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';
|
||||
|
||||
class DocumentPluginBuilder extends PluginBuilder {
|
||||
@ -137,9 +138,22 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
return BlocProvider<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DocumentShareButton(key: ValueKey(view.id), view: view),
|
||||
const HSpace(4),
|
||||
DocumentShareButton(
|
||||
key: ValueKey('share_button_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
...FeatureFlag.syncDocument.isOn
|
||||
? [
|
||||
const HSpace(20),
|
||||
DocumentSyncIndicator(
|
||||
key: ValueKey('sync_state_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
const HSpace(12),
|
||||
]
|
||||
: [const HSpace(8)],
|
||||
ViewFavoriteButton(
|
||||
key: ValueKey('favorite_button_${view.id}'),
|
||||
view: view,
|
||||
|
@ -175,20 +175,3 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentSyncIndicator extends StatelessWidget {
|
||||
const DocumentSyncIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentBloc, DocumentState>(
|
||||
builder: (context, state) {
|
||||
if (state.isSyncing) {
|
||||
return const SizedBox(height: 1, child: LinearProgressIndicator());
|
||||
} else {
|
||||
return const SizedBox(height: 1);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,66 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_sync_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentSyncIndicator extends StatelessWidget {
|
||||
const DocumentSyncIndicator({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
DocumentSyncBloc(view: view)..add(const DocumentSyncEvent.initial()),
|
||||
child: BlocBuilder<DocumentSyncBloc, DocumentSyncBlocState>(
|
||||
builder: (context, state) {
|
||||
// don't show indicator if user is local
|
||||
if (!state.shouldShowIndicator) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final Color color;
|
||||
final String hintText;
|
||||
|
||||
if (!state.isNetworkConnected) {
|
||||
color = Colors.grey;
|
||||
hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr();
|
||||
} else {
|
||||
switch (state.syncState) {
|
||||
case DocumentSyncState.SyncFinished:
|
||||
color = Colors.green;
|
||||
hintText = LocaleKeys.newSettings_syncState_synced.tr();
|
||||
break;
|
||||
case DocumentSyncState.Syncing:
|
||||
case DocumentSyncState.InitSyncBegin:
|
||||
color = Colors.yellow;
|
||||
hintText = LocaleKeys.newSettings_syncState_syncing.tr();
|
||||
break;
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
return FlowyTooltip(
|
||||
message: hintText,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -21,6 +21,10 @@ enum FeatureFlag {
|
||||
// if it's on, you can see the members settings in the settings page
|
||||
membersSettings,
|
||||
|
||||
// used to control the sync feature of the document
|
||||
// if it's on, the document will be synced the events from server in real-time
|
||||
syncDocument,
|
||||
|
||||
// used for ignore the conflicted feature flag
|
||||
unknown;
|
||||
|
||||
@ -82,6 +86,8 @@ enum FeatureFlag {
|
||||
return false;
|
||||
case FeatureFlag.membersSettings:
|
||||
return false;
|
||||
case FeatureFlag.syncDocument:
|
||||
return false;
|
||||
case FeatureFlag.unknown:
|
||||
return false;
|
||||
}
|
||||
@ -93,6 +99,8 @@ enum FeatureFlag {
|
||||
return 'if it\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app';
|
||||
case FeatureFlag.membersSettings:
|
||||
return 'if it\'s on, you can see the members settings in the settings page';
|
||||
case FeatureFlag.syncDocument:
|
||||
return 'if it\'s on, the document will be synced the events from server in real-time';
|
||||
case FeatureFlag.unknown:
|
||||
return '';
|
||||
}
|
||||
|
@ -463,14 +463,14 @@ GoRoute _signInScreenRoute() {
|
||||
|
||||
GoRoute _mobileEditorScreenRoute() {
|
||||
return GoRoute(
|
||||
path: MobileEditorScreen.routeName,
|
||||
path: MobileDocumentScreen.routeName,
|
||||
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.uri.queryParameters[MobileEditorScreen.viewId]!;
|
||||
final title = state.uri.queryParameters[MobileEditorScreen.viewTitle];
|
||||
final id = state.uri.queryParameters[MobileDocumentScreen.viewId]!;
|
||||
final title = state.uri.queryParameters[MobileDocumentScreen.viewTitle];
|
||||
|
||||
return MaterialExtendedPage(
|
||||
child: MobileEditorScreen(id: id, title: title),
|
||||
child: MobileDocumentScreen(id: id, title: title),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -12,26 +12,26 @@ Future<String> getDeviceId() async {
|
||||
return "test_device_id";
|
||||
}
|
||||
|
||||
String deviceId = "";
|
||||
String? deviceId;
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
deviceId = androidInfo.device;
|
||||
} else if (Platform.isIOS) {
|
||||
final IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||
deviceId = iosInfo.identifierForVendor ?? "";
|
||||
deviceId = iosInfo.identifierForVendor;
|
||||
} else if (Platform.isMacOS) {
|
||||
final MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;
|
||||
deviceId = macInfo.systemGUID ?? "";
|
||||
deviceId = macInfo.systemGUID;
|
||||
} else if (Platform.isWindows) {
|
||||
final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo;
|
||||
deviceId = windowsInfo.computerName;
|
||||
} else if (Platform.isLinux) {
|
||||
final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;
|
||||
deviceId = linuxInfo.machineId ?? "";
|
||||
deviceId = linuxInfo.machineId;
|
||||
}
|
||||
} on PlatformException {
|
||||
Log.error('Failed to get platform version');
|
||||
}
|
||||
return deviceId;
|
||||
return deviceId ?? '';
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const JsonEncoder _encoder = JsonEncoder.withIndent(' ');
|
||||
void prettyPrintJson(Object? object) {
|
||||
Log.trace(_encoder.convert(object));
|
||||
debugPrint(_encoder.convert(object));
|
||||
}
|
||||
|
@ -1415,6 +1415,11 @@
|
||||
},
|
||||
"language": "Language"
|
||||
}
|
||||
},
|
||||
"syncState": {
|
||||
"syncing": "Syncing",
|
||||
"synced": "Everything is up to date",
|
||||
"noNetworkConnected": "No network connected"
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ use event_integration::document_event::assert_document_data_equal;
|
||||
use event_integration::user_event::user_localhost_af_cloud;
|
||||
use event_integration::EventIntegrationTest;
|
||||
use flowy_core::DEFAULT_NAME;
|
||||
use flowy_document::entities::DocumentSyncStatePB;
|
||||
use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB};
|
||||
|
||||
use crate::util::{receive_with_timeout, unzip_history_user_db};
|
||||
|
||||
@ -30,7 +30,9 @@ async fn af_cloud_edit_document_test() {
|
||||
// wait all update are send to the remote
|
||||
let rx = test
|
||||
.notification_sender
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| !pb.is_syncing);
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| {
|
||||
pb.value != DocumentSyncState::Syncing
|
||||
});
|
||||
let _ = receive_with_timeout(rx, Duration::from_secs(30)).await;
|
||||
|
||||
let document_data = test.get_document_data(&document_id).await;
|
||||
@ -61,7 +63,9 @@ async fn af_cloud_sync_anon_user_document_test() {
|
||||
// wait all update are send to the remote
|
||||
let rx = test
|
||||
.notification_sender
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| !pb.is_syncing);
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| {
|
||||
pb.value != DocumentSyncState::Syncing
|
||||
});
|
||||
let _ = receive_with_timeout(rx, Duration::from_secs(30)).await;
|
||||
|
||||
let doc_state = test.get_document_doc_state(&document_id).await;
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use event_integration::document_event::assert_document_data_equal;
|
||||
use flowy_document::entities::DocumentSyncStatePB;
|
||||
use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB};
|
||||
|
||||
use crate::document::supabase_test::helper::FlowySupabaseDocumentTest;
|
||||
use crate::util::receive_with_timeout;
|
||||
@ -23,7 +23,9 @@ async fn supabase_document_edit_sync_test() {
|
||||
// wait all update are send to the remote
|
||||
let rx = test
|
||||
.notification_sender
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| !pb.is_syncing);
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| {
|
||||
pb.value != DocumentSyncState::Syncing
|
||||
});
|
||||
receive_with_timeout(rx, Duration::from_secs(30))
|
||||
.await
|
||||
.unwrap();
|
||||
@ -49,7 +51,9 @@ async fn supabase_document_edit_sync_test2() {
|
||||
// wait all update are send to the remote
|
||||
let rx = test
|
||||
.notification_sender
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| !pb.is_syncing);
|
||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| {
|
||||
pb.value != DocumentSyncState::Syncing
|
||||
});
|
||||
receive_with_timeout(rx, Duration::from_secs(30))
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -445,14 +445,27 @@ pub struct DocumentSnapshotStatePB {
|
||||
#[derive(Debug, Default, ProtoBuf)]
|
||||
pub struct DocumentSyncStatePB {
|
||||
#[pb(index = 1)]
|
||||
pub is_syncing: bool,
|
||||
pub value: DocumentSyncState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, ProtoBuf_Enum, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum DocumentSyncState {
|
||||
#[default]
|
||||
InitSyncBegin = 0,
|
||||
InitSyncEnd = 1,
|
||||
Syncing = 2,
|
||||
SyncFinished = 3,
|
||||
}
|
||||
|
||||
impl From<SyncState> for DocumentSyncStatePB {
|
||||
fn from(value: SyncState) -> Self {
|
||||
Self {
|
||||
is_syncing: value.is_syncing(),
|
||||
}
|
||||
let value = match value {
|
||||
SyncState::InitSyncBegin => DocumentSyncState::InitSyncBegin,
|
||||
SyncState::InitSyncEnd => DocumentSyncState::InitSyncEnd,
|
||||
SyncState::Syncing => DocumentSyncState::Syncing,
|
||||
SyncState::SyncFinished => DocumentSyncState::SyncFinished,
|
||||
};
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user