mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-28 05:34:41 +00:00
fix: v0.9.2 launch review issues (#7898)
* fix: remove empty paragraphs after inserting file block * fix: chat page animation * fix: remove opacity from locked page icon * feat: open the space after opening the view * fix: chat page loading issue * fix: view title assertion * fix: cover title issue * fix: flutter analyze * fix: list state issue * fix: reverse animation list issue * fix: integration test
This commit is contained in:
parent
0f89b6f690
commit
d3ce59fe08
@ -71,11 +71,6 @@ PODS:
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- Sentry/HybridSDK (8.46.0)
|
||||
- sentry_flutter (8.14.2):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.46.0)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@ -112,7 +107,6 @@ DEPENDENCIES:
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
@ -126,7 +120,6 @@ SPEC REPOS:
|
||||
- DKPhotoGallery
|
||||
- ReachabilitySwift
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
@ -165,8 +158,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
saver_gallery:
|
||||
:path: ".symlinks/plugins/saver_gallery/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@ -202,8 +193,6 @@ SPEC CHECKSUMS:
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/util/debounce.dart';
|
||||
import 'package:diffutil_dart/diffutil.dart' as diffutil;
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -43,7 +43,6 @@ class ChatAnimatedList extends StatefulWidget {
|
||||
|
||||
class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey();
|
||||
late final ChatController _chatController = Provider.of<ChatController>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -69,6 +68,10 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
int _lastUserMessageIndex = 0;
|
||||
bool _isScrollingToBottom = false;
|
||||
|
||||
final _loadPreviousMessagesDebounce = Debounce(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -134,15 +137,19 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
itemPositionsListener.itemPositions.addListener(() {
|
||||
_handleToggleScrollToBottom();
|
||||
});
|
||||
|
||||
itemPositionsListener.itemPositions.addListener(() {
|
||||
_handleLoadPreviousMessages();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
_scrollToBottomShowTimer?.cancel();
|
||||
_scrollToBottomController.dispose();
|
||||
_operationsSubscription.cancel();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -150,13 +157,21 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
final builders = context.watch<Builders>();
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
|
||||
return Stack(
|
||||
// A trick to avoid the first message being scrolled to the top
|
||||
int initialScrollIndex = _chatController.messages.length;
|
||||
double initialAlignment = 1.0;
|
||||
if (_chatController.messages.length <= 2) {
|
||||
initialScrollIndex = 0;
|
||||
initialAlignment = 0.0;
|
||||
}
|
||||
|
||||
final Widget child = Stack(
|
||||
children: [
|
||||
ScrollablePositionedList.builder(
|
||||
scrollOffsetController: scrollOffsetController,
|
||||
itemScrollController: itemScrollController,
|
||||
initialScrollIndex: _chatController.messages.length,
|
||||
initialAlignment: 1,
|
||||
initialScrollIndex: initialScrollIndex,
|
||||
initialAlignment: initialAlignment,
|
||||
scrollOffsetListener: scrollOffsetListener,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
physics: ClampingScrollPhysics(),
|
||||
@ -165,7 +180,7 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
itemCount: _chatController.messages.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _chatController.messages.length) {
|
||||
return VSpace(height - 400);
|
||||
return VSpace(height - 360);
|
||||
}
|
||||
|
||||
final message = _chatController.messages[index];
|
||||
@ -192,6 +207,8 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
void _scrollLastMessageToTop(Message data) {
|
||||
@ -202,7 +219,6 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
|
||||
if (_lastUserMessageIndex != lastUserMessageIndex) {
|
||||
// scroll the current message to the top
|
||||
Log.info('scrolling the last message to the top');
|
||||
itemScrollController.scrollTo(
|
||||
index: lastUserMessageIndex,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@ -238,9 +254,15 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
// get the max item
|
||||
final sortedItems = itemPositionsListener.itemPositions.value.toList()
|
||||
..sort((a, b) => a.index.compareTo(b.index));
|
||||
final maxItem = sortedItems.last;
|
||||
final maxItem = sortedItems.lastOrNull;
|
||||
|
||||
if (maxItem.index >= _chatController.messages.length - 1) {
|
||||
if (maxItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxItem.index > _chatController.messages.length - 1 ||
|
||||
(maxItem.index == _chatController.messages.length - 1 &&
|
||||
maxItem.itemTrailingEdge <= 1.01)) {
|
||||
_scrollToBottomShowTimer?.cancel();
|
||||
_scrollToBottomController.reverse();
|
||||
return;
|
||||
@ -254,33 +276,31 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
|
||||
});
|
||||
}
|
||||
|
||||
void _handleLoadPreviousMessages() {
|
||||
final sortedItems = itemPositionsListener.itemPositions.value.toList()
|
||||
..sort((a, b) => a.index.compareTo(b.index));
|
||||
final minItem = sortedItems.firstOrNull;
|
||||
|
||||
if (minItem == null || minItem.index > 0 || minItem.itemLeadingEdge < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
_loadPreviousMessagesDebounce.call(
|
||||
() {
|
||||
widget.onLoadPreviousMessages?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onInserted(final int position, final Message data) {
|
||||
if (position == _oldList.length) {
|
||||
_scrollLastMessageToTop(data);
|
||||
}
|
||||
}
|
||||
|
||||
void _onRemoved(final int position, final Message data) {
|
||||
final visualPosition = max(_oldList.length - position - 1, 0);
|
||||
_listKey.currentState!.removeItem(
|
||||
visualPosition,
|
||||
(context, animation) => widget.itemBuilder(
|
||||
context,
|
||||
animation,
|
||||
data,
|
||||
isRemoved: true,
|
||||
),
|
||||
duration: widget.removeAnimationDuration,
|
||||
);
|
||||
}
|
||||
void _onRemoved(final int position, final Message data) {}
|
||||
|
||||
void _onChanged(int position, Message oldData, Message newData) {
|
||||
_onRemoved(position, oldData);
|
||||
_listKey.currentState!.insertItem(
|
||||
max(_oldList.length - position - 1, 0),
|
||||
duration: widget.insertAnimationDuration,
|
||||
);
|
||||
}
|
||||
void _onChanged(int position, Message oldData, Message newData) {}
|
||||
|
||||
void _onDiffUpdate(diffutil.DataDiffUpdate<Message> update) {
|
||||
update.when<void>(
|
||||
|
||||
@ -168,7 +168,7 @@ class ChatAnimatedListReversedState extends State<ChatAnimatedListReversed>
|
||||
slivers: <Widget>[
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 400,
|
||||
top: widget.bottomPadding ?? 0,
|
||||
),
|
||||
),
|
||||
SliverAnimatedList(
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension InsertFile on EditorState {
|
||||
Future<void> insertEmptyFileBlock(GlobalKey key) async {
|
||||
@ -17,11 +16,18 @@ extension InsertFile on EditorState {
|
||||
}
|
||||
final file = fileNode(url: '')..extraInfos = {'global_key': key};
|
||||
|
||||
final insertedPath = delta.isEmpty ? path : path.next;
|
||||
final transaction = this.transaction
|
||||
..insertNode(insertedPath, file)
|
||||
..insertNode(insertedPath, paragraphNode())
|
||||
..afterSelection = Selection.collapsed(Position(path: insertedPath.next));
|
||||
final transaction = this.transaction;
|
||||
|
||||
// if current node is a paragraph and empty, replace it with the file block
|
||||
if (delta.isEmpty && node.type == ParagraphBlockKeys.type) {
|
||||
final insertedPath = path;
|
||||
transaction.insertNode(insertedPath, file);
|
||||
transaction.deleteNode(node);
|
||||
} else {
|
||||
// otherwise, insert the file block after the current node
|
||||
final insertedPath = path.next;
|
||||
transaction.insertNode(insertedPath, file);
|
||||
}
|
||||
|
||||
return apply(transaction);
|
||||
}
|
||||
|
||||
@ -52,6 +52,8 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||
late final titleFocusNode = editorContext.coverTitleFocusNode;
|
||||
int lineCount = 1;
|
||||
|
||||
bool updatingViewName = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -87,7 +89,7 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||
final width = context.read<DocumentAppearanceCubit>().state.width;
|
||||
return BlocConsumer<ViewBloc, ViewState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.view.name != current.view.name,
|
||||
previous.view.name != current.view.name && !updatingViewName,
|
||||
listener: _onListen,
|
||||
builder: (context, state) {
|
||||
final appearance = context.read<DocumentAppearanceCubit>().state;
|
||||
@ -206,6 +208,8 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||
}
|
||||
|
||||
void _onViewNameChanged() {
|
||||
updatingViewName = true;
|
||||
|
||||
Debounce.debounce(
|
||||
'update view name',
|
||||
const Duration(milliseconds: 250),
|
||||
@ -222,6 +226,8 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||
context
|
||||
.read<ViewInfoBloc?>()
|
||||
?.add(ViewInfoEvent.titleChanged(titleTextController.text));
|
||||
|
||||
updatingViewName = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
show UserProfilePB;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -116,6 +117,9 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
TabsEvent.openPlugin(plugin: view.plugin()),
|
||||
);
|
||||
}
|
||||
|
||||
// switch to the space that contains the last opened view
|
||||
_switchToSpace(view);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
||||
@ -290,6 +294,16 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _switchToSpace(ViewPB view) async {
|
||||
final ancestors = await ViewBackendService.getViewAncestors(view.id);
|
||||
final space = ancestors.fold(
|
||||
(ancestors) =>
|
||||
ancestors.items.firstWhereOrNull((ancestor) => ancestor.isSpace),
|
||||
(error) => null,
|
||||
);
|
||||
switchToSpaceIdNotifier.value = space;
|
||||
}
|
||||
}
|
||||
|
||||
class DesktopHomeScreenStackAdaptor extends HomeStackDelegate {
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_b
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
@ -18,6 +19,7 @@ typedef KeyDownHandler = void Function(HotKey hotKey);
|
||||
|
||||
ValueNotifier<int> switchToTheNextSpace = ValueNotifier(0);
|
||||
ValueNotifier<int> createNewPageNotifier = ValueNotifier(0);
|
||||
ValueNotifier<ViewPB?> switchToSpaceIdNotifier = ValueNotifier(null);
|
||||
|
||||
@visibleForTesting
|
||||
final zoomInKeyCodes = [KeyCode.equal, KeyCode.numpadAdd, KeyCode.add];
|
||||
|
||||
@ -73,7 +73,9 @@ class _SpaceState extends State<_Space> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
switchToTheNextSpace.addListener(_switchToNextSpace);
|
||||
switchToSpaceIdNotifier.addListener(_switchToSpace);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -81,6 +83,7 @@ class _SpaceState extends State<_Space> {
|
||||
switchToTheNextSpace.removeListener(_switchToNextSpace);
|
||||
isHovered.dispose();
|
||||
isExpandedNotifier.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -175,4 +178,17 @@ class _SpaceState extends State<_Space> {
|
||||
void _switchToNextSpace() {
|
||||
context.read<SpaceBloc>().add(const SpaceEvent.switchToNextSpace());
|
||||
}
|
||||
|
||||
void _switchToSpace() {
|
||||
if (!mounted || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final space = switchToSpaceIdNotifier.value;
|
||||
if (space == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<SpaceBloc>().add(SpaceEvent.open(space));
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,10 +109,7 @@ class LockPageButtonWrapper extends StatelessWidget {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.lockPage_lockedOperationTooltip.tr(),
|
||||
child: IgnorePointer(
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -136,9 +136,11 @@ class ViewTitleBar extends StatelessWidget {
|
||||
? ViewTitleBehavior.editable // only the last one is editable
|
||||
: ViewTitleBehavior.uneditable, // others are not editable
|
||||
onUpdated: () {
|
||||
context
|
||||
.read<ViewTitleBarBloc>()
|
||||
.add(const ViewTitleBarEvent.reload());
|
||||
if (context.mounted) {
|
||||
context
|
||||
.read<ViewTitleBarBloc>()
|
||||
.add(const ViewTitleBarEvent.reload());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('FileBlock:', () {
|
||||
test('insert file block in non-empty paragraph', () async {
|
||||
final document = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[paragraphNode(text: 'Hello World')],
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
editorState.selection = Selection.collapsed(Position(path: [0]));
|
||||
|
||||
// insert file block after the first line
|
||||
await editorState.insertEmptyFileBlock(GlobalKey());
|
||||
|
||||
final afterDocument = editorState.document;
|
||||
expect(afterDocument.root.children.length, 2);
|
||||
expect(afterDocument.root.children[1].type, FileBlockKeys.type);
|
||||
expect(afterDocument.root.children[0].type, ParagraphBlockKeys.type);
|
||||
expect(
|
||||
afterDocument.root.children[0].delta!.toPlainText(),
|
||||
'Hello World',
|
||||
);
|
||||
});
|
||||
|
||||
test('insert file block in empty paragraph', () async {
|
||||
final document = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[paragraphNode(text: '')],
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
editorState.selection = Selection.collapsed(Position(path: [0]));
|
||||
|
||||
await editorState.insertEmptyFileBlock(GlobalKey());
|
||||
|
||||
final afterDocument = editorState.document;
|
||||
expect(afterDocument.root.children.length, 1);
|
||||
expect(afterDocument.root.children[0].type, FileBlockKeys.type);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user