-
Notifications
You must be signed in to change notification settings - Fork 5
feat(#51): Implement Messages feature
#55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
992202d
3bc2288
be580f7
10b446b
8202ca6
7951ccc
a0a3979
87abe38
8064b5b
5e32c9d
2873dfc
35a3324
6e470ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| // ignore_for_file: implementation_imports, non_constant_identifier_names, depend_on_referenced_packages | ||
|
|
||
| /* | ||
| * ❌ GENERATED BY NSTACK, DO NOT MODIFY THIS FILE BY HAND! | ||
| * | ||
|
|
@@ -22,16 +24,23 @@ | |
| */ | ||
|
|
||
| import 'dart:async'; | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:flutter/cupertino.dart' as cupertino; | ||
| import 'package:flutter/foundation.dart'; | ||
| import 'package:flutter/material.dart' as material; | ||
| import 'package:flutter/widgets.dart'; | ||
| import 'package:nstack/models/app_open_platform.dart'; | ||
| import 'package:nstack/models/language.dart'; | ||
| import 'package:nstack/models/localize_index.dart'; | ||
| import 'package:nstack/models/message.dart'; | ||
| import 'package:nstack/models/nstack_config.dart'; | ||
| import 'package:nstack/sdk/nstack_sdk.dart'; | ||
| import 'package:nstack/sdk/localization/nstack_localization.dart'; | ||
| import 'package:nstack/nstack.dart'; | ||
| import 'package:nstack/partial/section_key_delegate.dart'; | ||
| import 'package:nstack/sdk/localization/nstack_localization.dart'; | ||
| import 'package:nstack/sdk/messages/nstack_messages.dart'; | ||
| import 'package:nstack/src/nstack_repository.dart'; | ||
| import 'package:url_launcher/url_launcher.dart'; | ||
|
|
||
| export 'package:nstack/models/app_open_platform.dart'; | ||
|
|
||
|
|
@@ -42,20 +51,27 @@ export 'package:nstack/models/app_open_platform.dart'; | |
| */ | ||
|
|
||
| final NStack = NStackSdk<Localization>( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it have to be a global variable? |
||
| config: _config, | ||
| localization: _nstackLocalization, | ||
| repository: _nstackRepository, | ||
| isDebug: kDebugMode, | ||
| localization: _nstackLocalization, | ||
| messages: _nstackMessages, | ||
| ); | ||
|
|
||
| const _nstackRepository = NStackRepository(_config); | ||
|
|
||
| final _nstackLocalization = NStackLocalization<Localization>( | ||
| config: _config, | ||
johsoe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| repository: _nstackRepository, | ||
| translations: const Localization(), | ||
| availableLanguages: _languages, | ||
| bundledTranslations: _bundledTranslations, | ||
| pickedLanguageLocale: '', | ||
| isDebug: kDebugMode, | ||
| ); | ||
|
|
||
| final _nstackMessages = NStackMessages( | ||
| repository: _nstackRepository, | ||
| ); | ||
|
|
||
| const _config = NStackConfig( | ||
| projectId: 'h6wJremI2TGFM88gbLkdyljWQuwf2hxhxvCH', | ||
| apiKey: 'zp2S18H32b67eYAbRQh94tVw76ZzaKKXlHjd', | ||
|
|
@@ -142,6 +158,8 @@ class _Test extends SectionKeyDelegate { | |
| * NStack Flutter Widgets | ||
| * | ||
| */ | ||
|
|
||
| /// Allows to access NStack features via a `BuildContext`. | ||
| class NStackScope extends InheritedWidget { | ||
| final NStackState state; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not being used I think it's safe to remove, plus storing a widget's state is not a best practice and might produce unpredictable results. |
||
| final String checksum; | ||
|
|
@@ -161,10 +179,27 @@ class NStackScope extends InheritedWidget { | |
| checksum != oldWidget.checksum; | ||
| } | ||
|
|
||
| /// Widget that is used for accessing NStack features from the widget tree | ||
| /// & listening for localization changes. | ||
| /// | ||
| /// Is required for all the children widgets like [NStackMessageListener] | ||
| /// | ||
| /// In your app, use the `builder` property like this: | ||
| /// ```dart | ||
| /// MaterialApp( | ||
| /// ... | ||
| /// builder: (context, child) { | ||
| /// return NStackWidget( | ||
| /// child: child!, | ||
| /// ); | ||
| /// }, | ||
| /// ... | ||
| /// ); | ||
| /// ``` | ||
| class NStackWidget extends StatefulWidget { | ||
| final Widget child; | ||
| final AppOpenPlatform? platformOverride; | ||
| final VoidCallback? onComplete; | ||
| final Widget child; | ||
|
|
||
| const NStackWidget({ | ||
| Key? key, | ||
|
|
@@ -185,6 +220,12 @@ class NStackState extends State<NStackWidget> { | |
|
|
||
| late final StreamSubscription _localeChangedSubscription; | ||
|
|
||
| /// Gets the NStack Localization feature configured for this project. | ||
| NStackLocalization<Localization> get localization => _nstack.localization; | ||
|
|
||
| /// Gets the NStack Message feature configured for this project. | ||
| NStackMessages get messages => _nstack.messages; | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
|
|
@@ -240,6 +281,215 @@ class NStackState extends State<NStackWidget> { | |
| } | ||
| } | ||
|
|
||
| /* | ||
| * | ||
| * NStack Messages Feature | ||
| * | ||
| */ | ||
|
|
||
| /// Listens for new messages from the NStack Messages feature. | ||
| /// | ||
| /// In where you want to use it, add this widget: | ||
| /// ```dart | ||
| /// Widget build(BuildContext context) { | ||
| /// return NStackMessageListener( | ||
| /// onMessage: (Message message) { | ||
| /// // Do whatever you want with the received message. | ||
| /// // For example, use NStackMessageDialog to display the message. | ||
| /// showDialog( | ||
| /// context: context, | ||
| /// builder: (context) { | ||
| /// return NStackMessageDialog(message: message); | ||
| /// }, | ||
| /// }, | ||
| /// child: Scaffold(...), | ||
| /// ); | ||
| /// } | ||
| /// ``` | ||
| class NStackMessageListener extends StatefulWidget { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we do factory methods on the main NStack class? Kind of like We could potentially extend that to have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @johsoe why?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No other reason than i like that pattern more than exposing multiple NStack* classes upfront which can be a bit messy to figure out
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @johsoe I guess you mean a named constructor, cause a factory on a class class NStack {
Widget NStack.Message() {
return Container();
}
}You will get the following errors:
To return a widget you can use a static method: class NStack {
static Widget message({Widget? child}) {
return child ?? Container();
}
}
// And then call `NStack.message(child: HomeScreen())` However, one interesting implementation I came up with is using a mixin: mixin NStackMessageListener<T extends StatefulWidget> on State<T> {
StreamSubscription? _subscription;
void onMessage(Message message) {}
@override
void didChangeDependencies() {
if (_subscription == null) {
final scope = NStackScope.of(context);
_subscription = scope.messages.onMessage.listen(onMessage);
}
super.didChangeDependencies();
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}And in your widget: class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> with NStackMessageListener {
@override
void onMessage(Message message) {
NStackMessageDialog.show(context, message: message);
super.onMessage(message);
}
@override
Widget build(BuildContext context) {
// ...
}
} |
||
| const NStackMessageListener({ | ||
| Key? key, | ||
| required this.onMessage, | ||
| this.child, | ||
| }) : super(key: key); | ||
|
|
||
| final void Function(Message message) onMessage; | ||
| final Widget? child; | ||
|
|
||
| @override | ||
| State<NStackMessageListener> createState() => _NStackMessageListenerState(); | ||
| } | ||
|
|
||
| class _NStackMessageListenerState extends State<NStackMessageListener> { | ||
| late final StreamSubscription _subscription; | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
|
|
||
| WidgetsBinding.instance.addPostFrameCallback((timeStamp) { | ||
| final scope = NStackScope.of(context); | ||
| _subscription = scope.messages.onMessage.listen(widget.onMessage); | ||
| }); | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| _subscription.cancel(); | ||
| super.dispose(); | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return widget.child ?? const SizedBox(); | ||
| } | ||
| } | ||
|
|
||
| /// Default Message Alert Dialog that adapts to the platform and renders the: | ||
| /// - Message body | ||
| /// - OK Button | ||
| /// - Open URL button (if [Message.url] is provided) | ||
| /// | ||
| /// When the dialog is dismissed, the alert reports that the message is viewed. | ||
| /// | ||
| /// Use it like this: | ||
| /// ```dart | ||
| /// NStackMessageDialog.show( | ||
| /// context, | ||
| /// message: message, | ||
| /// /* Other params if needed */ | ||
| /// ); | ||
| /// ``` | ||
| class NStackMessageDialog extends StatelessWidget { | ||
| static const _okButtonTitleFallback = 'OK'; | ||
| static const _openUrlButtonTitleFallback = 'Open URL'; | ||
| static const _dialogTitleFallback = 'Message'; | ||
|
|
||
| const NStackMessageDialog._({ | ||
| Key? key, | ||
| required this.message, | ||
| this.onOkPressed, | ||
| this.onOpenUrlPressed, | ||
| this.okButtonTitle = _okButtonTitleFallback, | ||
| this.openUrlButtonTitle = _openUrlButtonTitleFallback, | ||
| this.dialogTitle = _dialogTitleFallback, | ||
| }) : super(key: key); | ||
|
|
||
| /// Message that was received. | ||
| final Message message; | ||
|
|
||
| /// Title of the OK button. | ||
| final String okButtonTitle; | ||
|
|
||
| /// Title of the Open URL button. | ||
| final String openUrlButtonTitle; | ||
|
|
||
| /// Title of the dialog. | ||
| final String? dialogTitle; | ||
|
|
||
| /// Optional callback when a user presses the OK button. | ||
| /// | ||
| /// By default, it closes the dialog and reports that the message was viewed. | ||
| final VoidCallback? onOkPressed; | ||
|
|
||
| /// Optional callback when a user presses Open URL button. | ||
| /// | ||
| /// By default, it closes the dialog, reports that the message was viewed | ||
| /// and opens the URL. | ||
| final void Function(Uri uri)? onOpenUrlPressed; | ||
|
|
||
| /// Displays the dialog. | ||
| static Future<void> show( | ||
| BuildContext context, { | ||
| required Message message, | ||
| VoidCallback? onOkPressed, | ||
| void Function(Uri uri)? onOpenUrlPressed, | ||
| String? okButtonTitle, | ||
| String? openUrlButtonTitle, | ||
| String? dialogTitle = _dialogTitleFallback, | ||
| }) { | ||
| Widget builder(BuildContext context) { | ||
| return NStackMessageDialog._( | ||
| message: message, | ||
| onOkPressed: onOkPressed, | ||
| onOpenUrlPressed: onOpenUrlPressed, | ||
| okButtonTitle: okButtonTitle ?? | ||
| message.localization?.okBtn ?? | ||
| _okButtonTitleFallback, | ||
| openUrlButtonTitle: openUrlButtonTitle ?? | ||
| message.localization?.urlBtn ?? | ||
| _openUrlButtonTitleFallback, | ||
| dialogTitle: dialogTitle, | ||
| ); | ||
| } | ||
|
|
||
| final showDialog = Platform.isIOS | ||
| ? cupertino.showCupertinoDialog(context: context, builder: builder) | ||
| : material.showDialog(context: context, builder: builder); | ||
|
|
||
| return showDialog.whenComplete(() { | ||
| NStack.messages.setMessageViewed(message.id); | ||
| }); | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final titleWidget = dialogTitle != null ? Text(dialogTitle!) : null; | ||
| final messageWidget = Text(message.message); | ||
|
|
||
| final okWidget = Text(okButtonTitle); | ||
| final okAction = onOkPressed ?? Navigator.of(context).pop; | ||
|
|
||
| final messageUrl = message.url; | ||
| final uri = messageUrl != null ? Uri.tryParse(messageUrl) : null; | ||
|
|
||
| final isUriValid = uri != null; | ||
|
|
||
| final urlLaunchAction = !isUriValid | ||
| ? null | ||
| : (onOpenUrlPressed != null ? () => onOpenUrlPressed!(uri!) : null) ?? | ||
| () { | ||
| launchUrl(uri!); | ||
| Navigator.of(context).pop(); | ||
| }; | ||
|
|
||
| if (Platform.isIOS) { | ||
| return cupertino.CupertinoAlertDialog( | ||
| title: titleWidget, | ||
| content: messageWidget, | ||
| actions: [ | ||
| cupertino.CupertinoDialogAction( | ||
| onPressed: okAction, | ||
| child: okWidget, | ||
| ), | ||
| if (isUriValid) | ||
| cupertino.CupertinoDialogAction( | ||
| isDefaultAction: true, | ||
| onPressed: urlLaunchAction, | ||
| child: Text(openUrlButtonTitle), | ||
| ), | ||
| ], | ||
| ); | ||
| } | ||
|
|
||
| return material.AlertDialog( | ||
| title: titleWidget, | ||
| content: messageWidget, | ||
| actions: [ | ||
| if (isUriValid) | ||
| material.TextButton( | ||
| onPressed: urlLaunchAction, | ||
| child: Text(openUrlButtonTitle), | ||
| ), | ||
| material.TextButton( | ||
| onPressed: okAction, | ||
| child: okWidget, | ||
| ), | ||
| ], | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * | ||
| * NStack Flutter Extensions | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export 'package:nstack/sdk/nstack_sdk.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the onMessage can be added directly to
NStackWidget, hence no need for anNStackMessageListener.