diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml
index 4c4436244c..351994354d 100644
--- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml
+++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml
@@ -47,6 +47,12 @@
+
+
+
+
diff --git a/frontend/appflowy_flutter/ios/Podfile b/frontend/appflowy_flutter/ios/Podfile
index 131b4885ea..5e46cacdb4 100644
--- a/frontend/appflowy_flutter/ios/Podfile
+++ b/frontend/appflowy_flutter/ios/Podfile
@@ -37,6 +37,16 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
+
+ target.build_configurations.each do |config|
+ config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
+ '$(inherited)',
+
+ # dart: PermissionGroup.photos
+ 'PERMISSION_PHOTOS=1',
+ ]
+
+ end
end
installer.aggregate_targets.each do |target|
diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock
index ef1354faff..f734473d65 100644
--- a/frontend/appflowy_flutter/ios/Podfile.lock
+++ b/frontend/appflowy_flutter/ios/Podfile.lock
@@ -63,6 +63,8 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - permission_handler_apple (9.3.0):
+ - Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
@@ -98,6 +100,7 @@ DEPENDENCIES:
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@@ -144,6 +147,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
+ permission_handler_apple:
+ :path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -173,6 +178,7 @@ SPEC CHECKSUMS:
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
+ permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
@@ -183,6 +189,6 @@ SPEC CHECKSUMS:
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
-PODFILE CHECKSUM: d94f9be27d1db182e9bc77d10f065555d518f127
+PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.11.3
diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
index 2fae19e29d..aa53cf9b88 100644
--- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
+++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -127,6 +127,7 @@
97C146EC1CF9000F007C117D /* Resources */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
08FAA63113168DEC7FB74204 /* [CP] Embed Pods Frameworks */,
+ A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -233,6 +234,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
+ A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
E790B8FE5609053209ED85CB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart
index c11040682f..5a481eaa68 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart
@@ -1,53 +1,86 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
-///show the dialog to confirm one single action
-///[onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog
+enum ConfirmDialogActionAlignment {
+ // The action buttons are aligned vertically
+ // ---------------------
+ // | Action Button |
+ // | Cancel Button |
+ vertical,
+ // The action buttons are aligned horizontally
+ // ---------------------
+ // | Action Button | Cancel Button |
+ horizontal,
+}
+
+/// show the dialog to confirm one single action
+/// [onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog
Future showFlowyMobileConfirmDialog(
BuildContext context, {
Widget? title,
Widget? content,
+ ConfirmDialogActionAlignment actionAlignment =
+ ConfirmDialogActionAlignment.horizontal,
required String actionButtonTitle,
+ required VoidCallback? onActionButtonPressed,
Color? actionButtonColor,
String? cancelButtonTitle,
- required void Function()? onActionButtonPressed,
- void Function()? onCancelButtonPressed,
+ Color? cancelButtonColor,
+ VoidCallback? onCancelButtonPressed,
}) async {
return showDialog(
context: context,
builder: (dialogContext) {
final foregroundColor = Theme.of(context).colorScheme.onSurface;
+ final actionButton = TextButton(
+ child: FlowyText(
+ actionButtonTitle,
+ color: actionButtonColor ?? foregroundColor,
+ ),
+ onPressed: () {
+ onActionButtonPressed?.call();
+ // we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog.
+ Navigator.of(dialogContext).pop();
+ },
+ );
+ final cancelButton = TextButton(
+ child: FlowyText(
+ cancelButtonTitle ?? LocaleKeys.button_cancel.tr(),
+ color: cancelButtonColor ?? foregroundColor,
+ ),
+ onPressed: () {
+ onCancelButtonPressed?.call();
+ Navigator.of(dialogContext).pop();
+ },
+ );
+
+ final actions = switch (actionAlignment) {
+ ConfirmDialogActionAlignment.horizontal => [
+ actionButton,
+ cancelButton,
+ ],
+ ConfirmDialogActionAlignment.vertical => [
+ Column(
+ children: [
+ actionButton,
+ const Divider(height: 1, color: Colors.grey),
+ cancelButton,
+ ],
+ ),
+ ],
+ };
+
return AlertDialog.adaptive(
title: title,
content: content,
- actions: [
- TextButton(
- child: Text(
- actionButtonTitle,
- style: TextStyle(
- color: actionButtonColor ?? foregroundColor,
- ),
- ),
- onPressed: () {
- onActionButtonPressed?.call();
- // we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog.
- Navigator.of(dialogContext).pop();
- },
- ),
- TextButton(
- child: Text(
- cancelButtonTitle ?? LocaleKeys.button_cancel.tr(),
- style: TextStyle(
- color: foregroundColor,
- ),
- ),
- onPressed: () {
- onCancelButtonPressed?.call();
- Navigator.of(dialogContext).pop();
- },
- ),
- ],
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 24.0,
+ vertical: 4.0,
+ ),
+ actionsAlignment: MainAxisAlignment.center,
+ actions: actions,
);
},
);
diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart
index 7feae779cc..0f2635cec9 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart
@@ -182,14 +182,21 @@ class DocumentCollabAdapter {
);
for (final state in values) {
// the following code is only for version 1
- if (state.version != 1) {
+ if (state.version != 1 || state.metadata.isEmpty) {
return;
}
final uid = state.user.uid.toString();
final did = state.user.deviceId;
- final metadata = DocumentAwarenessMetadata.fromJson(
- jsonDecode(state.metadata),
- );
+ debugPrint('metadata: ${state.metadata}');
+ DocumentAwarenessMetadata metadata;
+ try {
+ metadata = DocumentAwarenessMetadata.fromJson(
+ jsonDecode(state.metadata),
+ );
+ } catch (e) {
+ Log.error('Failed to parse metadata: $e, ${state.metadata}');
+ continue;
+ }
final selectionColor = metadata.selectionColor.tryToColor();
final cursorColor = metadata.cursorColor.tryToColor();
if ((uid == userId && did == deviceId) ||
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart
index 3e5cb5416a..0b72fd60f0 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart
@@ -1,21 +1,28 @@
+import 'dart:async';
+
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
+import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy/shared/feedback_gesture_detector.dart';
+import 'package:appflowy/startup/tasks/device_info_task.dart';
import 'package:appflowy/user/application/user_service.dart';
+import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
+import 'package:permission_handler/permission_handler.dart';
class PageStyleCoverImage extends StatelessWidget {
PageStyleCoverImage({
@@ -114,9 +121,22 @@ class PageStyleCoverImage extends StatelessWidget {
}
Future _pickImage(BuildContext context) async {
- final result = await _imagePicker.pickImage(
- source: ImageSource.gallery,
- );
+ final photoPermission = await _checkPhotoPermission(context);
+ if (!photoPermission) {
+ Log.error('Has no permission to access the photo library');
+ return;
+ }
+
+ XFile? result;
+ try {
+ result = await _imagePicker.pickImage(
+ source: ImageSource.gallery,
+ );
+ } catch (e) {
+ Log.error('Error while picking image: $e');
+ return;
+ }
+
final path = result?.path;
if (path != null && context.mounted) {
final String? result;
@@ -204,6 +224,54 @@ class PageStyleCoverImage extends StatelessWidget {
},
);
}
+
+ Future _checkPhotoPermission(BuildContext context) async {
+ // check the permission first
+ final status = await Permission.photos.status;
+ // if the permission is permanently denied, we should open the app settings
+ if (status.isPermanentlyDenied && context.mounted) {
+ unawaited(
+ showFlowyMobileConfirmDialog(
+ context,
+ title: FlowyText.semibold(
+ LocaleKeys.pageStyle_photoPermissionTitle.tr(),
+ maxLines: 3,
+ textAlign: TextAlign.center,
+ ),
+ content: FlowyText(
+ LocaleKeys.pageStyle_photoPermissionDescription.tr(),
+ maxLines: 5,
+ textAlign: TextAlign.center,
+ fontSize: 12.0,
+ ),
+ actionAlignment: ConfirmDialogActionAlignment.vertical,
+ actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(),
+ actionButtonColor: Colors.blue,
+ cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(),
+ cancelButtonColor: Colors.blue,
+ onActionButtonPressed: () {
+ openAppSettings();
+ },
+ ),
+ );
+
+ return false;
+ } else if (status.isDenied) {
+ // https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937
+ Permission permission = Permission.photos;
+ if (defaultTargetPlatform == TargetPlatform.android &&
+ ApplicationInfo.androidSDKVersion <= 32) {
+ permission = Permission.storage;
+ }
+ // if the permission is denied, we should request the permission
+ final newStatus = await permission.request();
+ if (newStatus.isDenied) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
class _UnsplashCover extends StatelessWidget {
diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart
index 55316d2305..64f5914e7c 100644
--- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart
+++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart
@@ -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/shared/af_role_pb_extension.dart';
@@ -18,6 +16,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
@@ -215,6 +214,8 @@ class _InviteMemberState extends State<_InviteMember> {
context
.read()
.add(WorkspaceMemberEvent.inviteWorkspaceMember(email));
+ // clear the email field after inviting
+ _emailController.clear();
}
}
diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock
index 8ff82d3612..e804004a31 100644
--- a/frontend/appflowy_flutter/pubspec.lock
+++ b/frontend/appflowy_flutter/pubspec.lock
@@ -1329,6 +1329,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.3"
+ permission_handler:
+ dependency: "direct main"
+ description:
+ name: permission_handler
+ sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.3.1"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "12.0.6"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.4.4"
+ permission_handler_html:
+ dependency: transitive
+ description:
+ name: permission_handler_html
+ sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.1"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.1"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
petitparser:
dependency: transitive
description:
diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml
index 68bf48d9e3..4a6972baff 100644
--- a/frontend/appflowy_flutter/pubspec.yaml
+++ b/frontend/appflowy_flutter/pubspec.yaml
@@ -134,6 +134,7 @@ dependencies:
avatar_stack: ^1.2.0
numerus: ^2.1.2
flutter_animate: ^4.5.0
+ permission_handler: ^11.3.1
dev_dependencies:
flutter_lints: ^3.0.1
diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json
index c09c4b5e9f..db3c5b14af 100644
--- a/frontend/resources/translations/en.json
+++ b/frontend/resources/translations/en.json
@@ -1534,7 +1534,11 @@
"photo": "Photo",
"unsplash": "Unsplash",
"pageCover": "Page cover",
- "none": "None"
+ "none": "None",
+ "photoPermissionDescription": "Allow access to the photo library for uploading images.",
+ "openSettings": "Open Settings",
+ "photoPermissionTitle": "AppFlowy Would Like to Access Your Photo Library",
+ "doNotAllow": "Don't Allow"
},
"commandPalette": {
"placeholder": "Type to search for views...",