Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/lib/app_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class AppScaffold extends StatelessWidget {

''',
),
showNotifications: true,
themeToggle: const SolidThemeToggleConfig(
enabled: true,
showInAppBarActions: true,
Expand Down
183 changes: 183 additions & 0 deletions example/lib/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,176 @@ class HomeState extends State<Home> with SingleTickerProviderStateMixin {
);
}

Future<void> _showSendNotificationDialog() async {
final loggedIn = await loginIfRequired(context);
if (!loggedIn) return;

final recipientController = TextEditingController();
final titleController = TextEditingController();
final contentController = TextEditingController();
int selectedPriority = 1;
String? recipientError;
String? titleError;

await showDialog(
context: context,
builder: (dialogContext) {
return StatefulBuilder(
builder: (stfContext, setDialogState) {
return AlertDialog(
title: const Text('Send Notification'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: recipientController,
decoration: InputDecoration(
labelText: 'Recipient WebID *',
errorText: recipientError,
),
),
const SizedBox(height: 12),
TextField(
controller: titleController,
decoration: InputDecoration(
labelText: 'Title *',
errorText: titleError,
),
),
const SizedBox(height: 12),
TextField(
controller: contentController,
decoration: const InputDecoration(
labelText: 'Content (optional)',
),
maxLines: 3,
),
const SizedBox(height: 12),
DropdownButtonFormField<int>(
initialValue: selectedPriority,
decoration: const InputDecoration(
labelText: 'Priority',
),
items: const [
DropdownMenuItem(value: 0, child: Text('Low')),
DropdownMenuItem(value: 1, child: Text('Medium')),
DropdownMenuItem(value: 2, child: Text('High')),
],
onChanged: (value) {
setDialogState(() {
selectedPriority = value ?? 1;
});
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
final recipient = recipientController.text.trim();
final notifTitle = titleController.text.trim();

final hasErrors = recipient.isEmpty || notifTitle.isEmpty;

setDialogState(() {
recipientError = recipient.isEmpty
? 'Recipient WebID is required'
: null;
titleError =
notifTitle.isEmpty ? 'Title is required' : null;
});

if (hasErrors) return;

Navigator.pop(dialogContext);

try {
await sendNotification(
recipientWebId: recipient,
title: notifTitle,
content: contentController.text.trim().isEmpty
? null
: contentController.text.trim(),
priority: selectedPriority,
);

if (context.mounted) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Success'),
content: const Text(
'Notification sent successfully.',
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
}
} on RecipientNotReadyException catch (e) {
debugPrint('Recipient not ready: $e');
if (context.mounted) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Recipient Not Ready'),
content: Text(
'Could not send notification to $recipient.\n\n'
'The recipient may need to log in and update '
'their app setup in their Pod before you can '
'send notifications to them.\n\n'
'Details: $e',
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
}
} on Exception catch (e) {
debugPrint('Failed to send notification: $e');
if (context.mounted) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Error'),
content: Text(
'Failed to send notification:\n$e',
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
}
}
},
child: const Text('Send'),
),
],
);
},
);
},
);
}

Widget _sectionHeading(String title, {Widget? trailing}) {
return Row(
children: [
Expand Down Expand Up @@ -459,6 +629,19 @@ class HomeState extends State<Home> with SingleTickerProviderStateMixin {

largeGapV,

// Notifications section.

_sectionHeading('Notifications'),
smallGapV,
_buttonRow([
ElevatedButton(
onPressed: _showSendNotificationDialog,
child: const Text('Send Notification'),
),
]),

largeGapV,

// Local Security Key Management section.

_sectionHeading('Local Security Key Management'),
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dependency_overrides:
solidpod:
git:
url: https://github.com/anusii/solidpod.git
ref: dev
ref: tony/257_notification
solidui:
path: ..

Expand Down
3 changes: 3 additions & 0 deletions lib/solidui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export 'src/widgets/solid_preferences_dialog.dart';
export 'src/widgets/solid_about_models.dart';
export 'src/widgets/solid_about_button.dart';

export 'src/widgets/solid_notification_button.dart';
export 'src/widgets/solid_notification_centre.dart';

export 'src/widgets/solid_security_key_utils.dart';
export 'src/widgets/solid_security_key_manager.dart';
export 'src/widgets/solid_security_key_view.dart';
Expand Down
66 changes: 65 additions & 1 deletion lib/src/widgets/grant_permission_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

library;

import 'dart:convert';

import 'package:flutter/material.dart';

import 'package:solidpod/solidpod.dart';
Expand Down Expand Up @@ -124,6 +126,11 @@ class GrantPermissionForm extends StatefulWidget {

final VoidCallback? onPermissionGranted;

/// Optional human-readable name for the resource, used in notification
/// messages sent to recipients upon successful permission granting.

final String? resourceDisplayName;

const GrantPermissionForm({
super.key,
required this.updatePermissionsFunction,
Expand All @@ -137,6 +144,7 @@ class GrantPermissionForm extends StatefulWidget {
required this.updatePermissionGrantedFunction,
this.dataFilesMap = const {},
this.onPermissionGranted,
this.resourceDisplayName,
});

@override
Expand Down Expand Up @@ -384,9 +392,65 @@ class _GrantPermissionFormState extends State<GrantPermissionForm> {

if (result == SolidFunctionCallStatus.success) {
_showSnackBar(successMsg, ActionColors.success);

// Send notification to each specific recipient (individual
// or group members) in the background. Non-specific types
// such as public or authenticated users are skipped.

if (selectedRecipientType == RecipientType.individual ||
selectedRecipientType == RecipientType.group) {
final displayName =
widget.resourceDisplayName ?? widget.resourceName;

final permissions = selectedPermList.join(', ');

final notificationFailures = <String>[];

for (final recipientWebId in finalWebIdList) {
try {
await sendNotification(
recipientWebId: recipientWebId as String,
title: 'Shared to you: $displayName',
content: jsonEncode({
'fileUrl': widget.resourceName,
'fileTitle': displayName,
'sharedBy': widget.granterWebId,
'owner': widget.ownerWebId,
'permissions': permissions,
}),
priority: 1,
);
} on RecipientNotReadyException catch (e) {
debugPrint(
'[GrantPermissionForm] '
'Recipient not ready for $recipientWebId: $e',
);
notificationFailures.add(
recipientWebId as String,
);
} on Object catch (e) {
debugPrint(
'[GrantPermissionForm] '
'Failed to send notification to $recipientWebId: $e',
);
}
}

if (notificationFailures.isNotEmpty) {
final names = notificationFailures.join(', ');
_showSnackBar(
'Permission granted, but could not notify: $names. '
'The recipient(s) may need to log in and update '
'their app setup in their Pod first.',
ActionColors.warning,
duration: const Duration(seconds: 8),
);
}
}

// Update permissions table
await widget.updatePermissionsFunction(
widget.resourceName, //_resourceName,
widget.resourceName,
isFile: widget.isFile,
isExternalRes: widget.isExternalRes,
);
Expand Down
7 changes: 7 additions & 0 deletions lib/src/widgets/grant_permission_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class GrantPermissionUi extends StatefulWidget {
this.customAppBar,
this.onPermissionGranted,
this.onNavigateBack,
this.resourceDisplayName,
super.key,
}) : assert(
// Requires ownerWebId if resource
Expand Down Expand Up @@ -170,6 +171,12 @@ class GrantPermissionUi extends StatefulWidget {

final VoidCallback? onNavigateBack;

/// Optional human-readable name for the resource, used in notification
/// messages sent to recipients upon successful permission granting.
/// Falls back to [resourceName] when not provided.

final String? resourceDisplayName;

@override
GrantPermissionUiState createState() => GrantPermissionUiState();
}
1 change: 1 addition & 0 deletions lib/src/widgets/grant_permission_ui_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ class GrantPermissionUiState extends State<GrantPermissionUi>
isFile: widget.isFile,
dataFilesMap: widget.dataFilesMap,
onPermissionGranted: widget.onPermissionGranted,
resourceDisplayName: widget.resourceDisplayName,
),
mediumGapV,
makeSubHeading(
Expand Down
9 changes: 9 additions & 0 deletions lib/src/widgets/share_resource_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import 'package:solidui/src/widgets/grant_permission_form.dart';
/// - [onPermissionGranted] - Callback function called when permissions are granted successfully.

class ShareResourceButton extends StatefulWidget {
/// Text editing controller for filename

final TextEditingController fileNameController;

/// String to assign the webId of the resource owner.
Expand Down Expand Up @@ -102,6 +104,11 @@ class ShareResourceButton extends StatefulWidget {

final VoidCallback? onPermissionGranted;

/// Optional human-readable name for the resource, used in notification
/// messages sent to recipients upon successful permission granting.

final String? resourceDisplayName;

const ShareResourceButton({
super.key,
required this.fileNameController,
Expand All @@ -115,6 +122,7 @@ class ShareResourceButton extends StatefulWidget {
required this.isFile,
this.dataFilesMap = const {},
this.onPermissionGranted,
this.resourceDisplayName,
});

@override
Expand Down Expand Up @@ -203,6 +211,7 @@ class _ShareResourceButtonState extends State<ShareResourceButton> {
updatePermissionGrantedFunction:
_updatePermissionGrantedStatus,
onPermissionGranted: widget.onPermissionGranted,
resourceDisplayName: widget.resourceDisplayName,
);
},
);
Expand Down
Loading
Loading