diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 6398500f7..e12ec2356 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4998,5 +4998,15 @@ "canBeFoundViaKnock": "\u2022 request to join and admin approval", "anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!", "createYourSpace": "Create your space", - "sendActivities": "Send activities" + "sendActivities": "Send activities", + "getStarted": "Get Started", + "getStartedBotChatDesc": "Chatting with AI is a great place to start and Pangea reading, writing, listening and speaking tools make it easy!", + "getStartedCommunitiesDesc": "Learning with a community is where Pangea Chat shines!\nYou can join your class, find a school, or even make your own!", + "getStartedFriendsDesc": "Do you have a friend that wants to learn with you?", + "getStartedBotChatComplete": "Well-done! You're chatting with the bot!", + "getStartedCommunitiesComplete": "Great, you have joined a space!", + "getStartedComplete": "You've completed this section!\nKeep exploring our amazing features by chatting with friends!", + "getStartedFriendsComplete": "Woohoo! You've got friends! 😉", + "getStartedBotChatButton": "Start chatting!", + "getStartedFriendsButton": "Invite a friend" } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 15937973e..1b647b2ae 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -40,6 +40,7 @@ import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart'; import 'package:fluffychat/pangea/login/pages/signup.dart'; import 'package:fluffychat/pangea/login/pages/space_code_onboarding.dart'; import 'package:fluffychat/pangea/login/pages/user_settings.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; import 'package:fluffychat/pangea/spaces/utils/join_with_alias.dart'; import 'package:fluffychat/pangea/spaces/utils/join_with_link.dart'; @@ -238,7 +239,7 @@ abstract class AppRoutes { FluffyThemes.isColumnMode(context) // #Pangea // ? const EmptyPage() - ? const SuggestionsPage() + ? const Onboarding() // Pangea# : ChatList( activeChat: state.pathParameters['roomid'], diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 4fa105675..4e3e468da 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/space_view.dart'; import 'package:fluffychat/pangea/chat_list/widgets/pangea_chat_list_header.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart'; @@ -287,6 +288,16 @@ class ChatListViewBody extends StatelessWidget { ); }, ), + // #Pangea + const SliverPadding(padding: EdgeInsets.all(12.0)), + if (!FluffyThemes.isColumnMode(context)) + SliverList.builder( + itemCount: 1, + itemBuilder: (context, _) { + return const Onboarding(); + }, + ), + // Pangea# ], ), ); diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 72e22c76d..9b090f207 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -7,6 +7,8 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pangea/chat_list/widgets/chat_list_view_body_wrapper.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_steps_enum.dart'; import 'package:fluffychat/widgets/navigation_rail.dart'; class ChatListView extends StatelessWidget { @@ -59,9 +61,14 @@ class ChatListView extends StatelessWidget { // #Pangea // body: ChatListViewBody(controller), body: ChatListViewBodyWrapper(controller: controller), - // Pangea# + // floatingActionButton: !controller.isSearchMode && + // controller.activeSpaceId == null floatingActionButton: !controller.isSearchMode && - controller.activeSpaceId == null + controller.activeSpaceId == null && + OnboardingController.complete( + OnboardingStepsEnum.chatWithBot, + ) + // Pangea# ? FloatingActionButton.extended( onPressed: () => context.go('/rooms/newprivatechat'), icon: const Icon(Icons.add_outlined), diff --git a/lib/pages/chat_list/navi_rail_item.dart b/lib/pages/chat_list/navi_rail_item.dart index 620d23cf2..f221955e5 100644 --- a/lib/pages/chat_list/navi_rail_item.dart +++ b/lib/pages/chat_list/navi_rail_item.dart @@ -116,8 +116,12 @@ class NaviRailItem extends StatelessWidget { : UnreadRoomsBadge( filter: unreadBadgeFilter, badgePosition: BadgePosition.topEnd( - top: -12, - end: -8, + // #Pangea + // top: -12, + // end: -8, + top: -20, + end: -16, + // Pangea# ), child: icon, ), diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 7c1481c88..70ece9b0c 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -17,6 +17,7 @@ import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart'; @@ -952,6 +953,16 @@ class _SpaceViewState extends State { ); }, ), + // #Pangea + const SliverPadding(padding: EdgeInsets.all(12.0)), + if (!FluffyThemes.isColumnMode(context)) + SliverList.builder( + itemCount: 1, + itemBuilder: (context, _) { + return const Onboarding(); + }, + ), + // Pangea# const SliverPadding(padding: EdgeInsets.only(top: 32)), ], ); diff --git a/lib/pangea/chat_list/widgets/pangea_chat_list_header.dart b/lib/pangea/chat_list/widgets/pangea_chat_list_header.dart index 51dff54fe..00c3b6e41 100644 --- a/lib/pangea/chat_list/widgets/pangea_chat_list_header.dart +++ b/lib/pangea/chat_list/widgets/pangea_chat_list_header.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_steps_enum.dart'; class PangeaChatListHeader extends StatelessWidget implements PreferredSizeWidget { @@ -33,43 +36,51 @@ class PangeaChatListHeader extends StatelessWidget child: Column( children: [ const LearningProgressIndicators(), - TextField( - controller: controller.searchController, - focusNode: controller.searchFocusNode, - textInputAction: TextInputAction.search, - onChanged: (text) => controller.onSearchEnter( - text, - globalSearch: globalSearch, - ), - decoration: InputDecoration( - filled: true, - fillColor: theme.colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - contentPadding: EdgeInsets.zero, - hintText: L10n.of(context).search, - hintStyle: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.normal, - ), - floatingLabelBehavior: FloatingLabelBehavior.never, - prefixIcon: controller.isSearchMode - ? IconButton( - tooltip: L10n.of(context).cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelSearch, - color: theme.colorScheme.onPrimaryContainer, - ) - : IconButton( - onPressed: controller.startSearch, - icon: Icon( - Icons.search_outlined, + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: OnboardingController.complete( + OnboardingStepsEnum.joinSpace, + ) + ? TextField( + controller: controller.searchController, + focusNode: controller.searchFocusNode, + textInputAction: TextInputAction.search, + onChanged: (text) => controller.onSearchEnter( + text, + globalSearch: globalSearch, + ), + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context).search, + hintStyle: TextStyle( color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: controller.isSearchMode + ? IconButton( + tooltip: L10n.of(context).cancel, + icon: const Icon(Icons.close_outlined), + onPressed: controller.cancelSearch, + color: theme.colorScheme.onPrimaryContainer, + ) + : IconButton( + onPressed: controller.startSearch, + icon: Icon( + Icons.search_outlined, + color: + theme.colorScheme.onPrimaryContainer, + ), + ), ), - ), + ) + : const SizedBox.shrink(), ), ], ), diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index c1c609697..546655323 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -10,13 +9,8 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; -import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; -import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; -import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart'; -import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/choreographer/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/choreographer/controllers/word_net_controller.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/controllers/message_data_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; @@ -81,10 +75,7 @@ class PangeaController { putAnalytics.initialize(); getAnalytics.initialize(); subscriptionController.initialize(); - - startChatWithBotIfNotPresent(); setPangeaPushRules(); - // joinSupportSpace(); } /// Initialize controllers @@ -203,158 +194,6 @@ class PangeaController { await getAnalytics.initialize(); } - void startChatWithBotIfNotPresent() { - Future.delayed(const Duration(milliseconds: 10000), () async { - // check if user is logged in - if (!matrixState.client.isLogged() || - matrixState.client.userID == null || - matrixState.client.userID == BotName.byEnvironment) { - return; - } - - final List botDMs = []; - for (final room in matrixState.client.rooms) { - if (await room.isBotDM) { - botDMs.add(room); - } - } - - if (botDMs.isEmpty) { - try { - // Copied from client.dart.startDirectChat - final directChatRoomId = - matrixState.client.getDirectChatFromUserId(BotName.byEnvironment); - if (directChatRoomId != null) { - final room = matrixState.client.getRoomById(directChatRoomId); - if (room != null) { - if (room.membership == Membership.join) { - return null; - } else if (room.membership == Membership.invite) { - // we might already have an invite into a DM room. If that is the case, we should try to join. If the room is - // unjoinable, that will automatically leave the room, so in that case we need to continue creating a new - // room. (This implicitly also prevents the room from being returned as a DM room by getDirectChatFromUserId, - // because it only returns joined or invited rooms atm.) - await room.join(); - if (room.membership != Membership.leave) { - if (room.membership != Membership.join) { - // Wait for room actually appears in sync with the right membership - await matrixState.client - .waitForRoomInSync(directChatRoomId, join: true); - } - return null; - } - } - } - } - // enableEncryption ??= - // encryptionEnabled && await userOwnsEncryptionKeys(mxid); - // if (enableEncryption) { - // initialState ??= []; - // if (!initialState.any((s) => s.type == EventTypes.Encryption)) { - // initialState.add( - // StateEvent( - // content: { - // 'algorithm': supportedGroupEncryptionAlgorithms.first, - // }, - // type: EventTypes.Encryption, - // ), - // ); - // } - // } - - // Start a new direct chat - final roomId = await matrixState.client.createRoom( - invite: [], // intentionally not invite bot yet - isDirect: true, - preset: CreateRoomPreset.trustedPrivateChat, - initialState: [ - BotOptionsModel(mode: BotMode.directChat).toStateEvent, - RoomDefaults.defaultPowerLevels( - matrixState.client.userID!, - ), - ], - ); - - Room? room = matrixState.client.getRoomById(roomId); - if (room == null || room.membership != Membership.join) { - // Wait for room actually appears in sync - await matrixState.client.waitForRoomInSync(roomId, join: true); - room = matrixState.client.getRoomById(roomId); - if (room == null) { - ErrorHandler.logError( - e: "Bot chat null after waiting for room in sync", - data: { - "roomId": roomId, - }, - ); - return null; - } - } - - final botOptions = room.getState(PangeaEventTypes.botOptions); - if (botOptions == null) { - await matrixState.client.setRoomStateWithKey( - roomId, - PangeaEventTypes.botOptions, - "", - BotOptionsModel(mode: BotMode.directChat).toJson(), - ); - await matrixState.client - .getRoomStateWithKey(roomId, PangeaEventTypes.botOptions, ""); - } - - // invite bot to direct chat - await matrixState.client.setRoomStateWithKey( - roomId, EventTypes.RoomMember, BotName.byEnvironment, { - "membership": Membership.invite.name, - "is_direct": true, - }); - await room.addToDirectChat(BotName.byEnvironment); - - return null; - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError( - e: err, - s: stack, - data: { - "directChatRoomId": matrixState.client - .getDirectChatFromUserId(BotName.byEnvironment), - }, - ); - } - } - - final Room botDMWithLatestActivity = botDMs.reduce((a, b) { - if (a.timeline == null || - b.timeline == null || - a.timeline!.events.isEmpty || - b.timeline!.events.isEmpty) { - return a; - } - final aLastEvent = a.timeline!.events.last; - final bLastEvent = b.timeline!.events.last; - return aLastEvent.originServerTs.isAfter(bLastEvent.originServerTs) - ? a - : b; - }); - - for (final room in botDMs) { - if (room.id != botDMWithLatestActivity.id) { - await room.leave(); - continue; - } - } - - final participants = await botDMWithLatestActivity.requestParticipants(); - final joinedParticipants = - participants.where((e) => e.membership == Membership.join).toList(); - if (joinedParticipants.length < 2) { - await botDMWithLatestActivity.invite(BotName.byEnvironment); - } - }); - } - void _subscribeToStreams() { matrixState.client.onLoginStateChanged.stream .listen(_handleLoginStateChange); diff --git a/lib/pangea/onboarding/onboarding.dart b/lib/pangea/onboarding/onboarding.dart new file mode 100644 index 000000000..1ef67ed57 --- /dev/null +++ b/lib/pangea/onboarding/onboarding.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import 'package:get_storage/get_storage.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; +import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; +import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart'; +import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_steps_enum.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_view.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class Onboarding extends StatefulWidget { + const Onboarding({super.key}); + + @override + OnboardingController createState() => OnboardingController(); +} + +class OnboardingController extends State { + static final GetStorage _onboardingStorage = GetStorage('onboarding_storage'); + + static bool get isClosed => _onboardingStorage.read('closed') ?? false; + + static bool get isComplete => OnboardingStepsEnum.values.every( + (step) => complete(step), + ); + + static bool complete(OnboardingStepsEnum step) { + switch (step) { + case OnboardingStepsEnum.chatWithBot: + return hasBotDM; + case OnboardingStepsEnum.joinSpace: + return MatrixState.pangeaController.matrixState.client.rooms.any( + (r) => r.isSpace, + ); + case OnboardingStepsEnum.inviteFriends: + return hasInvitedFriends; + } + } + + static bool get hasInvitedFriends => + _onboardingStorage.read('invite_friends') ?? false; + + static bool get hasBotDM => + MatrixState.pangeaController.matrixState.client.rooms.any((room) { + if (room.isDirectChat && + room.directChatMatrixID == BotName.byEnvironment) { + return true; + } + if (room.botOptions?.mode == BotMode.directChat) { + return true; + } + return false; + }); + + Future closeCompletedMessage() async { + await _onboardingStorage.write('closed', true); + if (mounted) setState(() {}); + } + + Future inviteFriends() async { + FluffyShare.shareInviteLink(context); + await _onboardingStorage.write('invite_friends', true); + if (mounted) setState(() {}); + } + + Future startChatWithBot() async { + final resp = await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context).client.createRoom( + invite: [BotName.byEnvironment], + isDirect: true, + preset: CreateRoomPreset.trustedPrivateChat, + initialState: [ + BotOptionsModel(mode: BotMode.directChat).toStateEvent, + RoomDefaults.defaultPowerLevels( + Matrix.of(context).client.userID!, + ), + ], + ), + ); + if (resp.isError) return; + context.go("/rooms/${resp.result}"); + } + + void joinCommunities() { + context.go('/rooms/communities'); + } + + Future onPressed(OnboardingStepsEnum step) async { + switch (step) { + case OnboardingStepsEnum.chatWithBot: + return startChatWithBot(); + case OnboardingStepsEnum.joinSpace: + return joinCommunities(); + case OnboardingStepsEnum.inviteFriends: + return inviteFriends(); + } + } + + @override + Widget build(BuildContext context) => OnboardingView(controller: this); +} diff --git a/lib/pangea/onboarding/onboarding_complete.dart b/lib/pangea/onboarding/onboarding_complete.dart new file mode 100644 index 000000000..ef1301325 --- /dev/null +++ b/lib/pangea/onboarding/onboarding_complete.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_constants.dart'; + +class OnboardingComplete extends StatelessWidget { + final OnboardingController controller; + const OnboardingComplete({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return FluffyThemes.isColumnMode(context) + ? Text( + L10n.of(context).getStartedComplete, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 32.0, + ), + ) + : Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface.withAlpha(20), + borderRadius: BorderRadius.circular( + 10.0, + ), + ), + margin: const EdgeInsets.all(12.0), + padding: const EdgeInsets.fromLTRB( + 48.0, + 8.0, + 48.0, + 0.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 24.0, + children: [ + Text( + L10n.of(context).getStartedComplete, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14.0, + ), + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${OnboardingConstants.onboardingImageFileName}", + fit: BoxFit.cover, + ), + ], + ), + ), + Positioned( + right: 16.0, + top: 16.0, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: controller.closeCompletedMessage, + ), + ), + ], + ); + } +} diff --git a/lib/pangea/onboarding/onboarding_constants.dart b/lib/pangea/onboarding/onboarding_constants.dart new file mode 100644 index 000000000..744acec18 --- /dev/null +++ b/lib/pangea/onboarding/onboarding_constants.dart @@ -0,0 +1,3 @@ +class OnboardingConstants { + static String onboardingImageFileName = "Getting+Started.png"; +} diff --git a/lib/pangea/onboarding/onboarding_step.dart b/lib/pangea/onboarding/onboarding_step.dart new file mode 100644 index 000000000..0b78884f0 --- /dev/null +++ b/lib/pangea/onboarding/onboarding_step.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_steps_enum.dart'; + +class OnboardingStep extends StatelessWidget { + final OnboardingStepsEnum step; + + final bool isComplete; + final VoidCallback onPressed; + + const OnboardingStep({ + super.key, + required this.step, + this.isComplete = false, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final isColumnMode = FluffyThemes.isColumnMode(context); + + return Container( + padding: EdgeInsets.symmetric( + horizontal: isColumnMode ? 20.0 : 8.0, + vertical: isColumnMode ? 24.0 : 8.0, + ), + margin: isColumnMode + ? const EdgeInsets.only( + bottom: 10.0, + ) + : const EdgeInsets.all(0.0), + decoration: isColumnMode && isComplete + ? ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 1, + color: AppConfig.success, + ), + borderRadius: BorderRadius.circular( + 24, + ), + ), + ) + : null, + child: Row( + spacing: isColumnMode ? 24.0 : 12.0, + children: [ + Icon( + Icons.task_alt, + size: isColumnMode ? 30.0 : 18.0, + color: isComplete + ? AppConfig.success + : Theme.of(context).colorScheme.primary, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: isColumnMode ? 16.0 : 8.0, + children: [ + Text( + isComplete + ? step.completeMessage( + L10n.of(context), + ) + : step.description( + L10n.of(context), + ), + style: TextStyle( + fontSize: isColumnMode ? 20.0 : 12.0, + ), + ), + if (!isComplete) + ElevatedButton( + onPressed: onPressed, + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + step.icon(18.0), + Text( + step.buttonText( + L10n.of( + context, + ), + ), + style: const TextStyle( + fontSize: 14.0, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/onboarding/onboarding_steps_enum.dart b/lib/pangea/onboarding/onboarding_steps_enum.dart new file mode 100644 index 000000000..cfe3c7d93 --- /dev/null +++ b/lib/pangea/onboarding/onboarding_steps_enum.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart'; + +enum OnboardingStepsEnum { + chatWithBot, + joinSpace, + inviteFriends; + + String description(L10n l10n) { + switch (this) { + case OnboardingStepsEnum.chatWithBot: + return l10n.getStartedBotChatDesc; + case OnboardingStepsEnum.joinSpace: + return l10n.getStartedCommunitiesDesc; + case OnboardingStepsEnum.inviteFriends: + return l10n.getStartedFriendsDesc; + } + } + + String completeMessage(L10n l10n) { + switch (this) { + case OnboardingStepsEnum.chatWithBot: + return l10n.getStartedBotChatComplete; + case OnboardingStepsEnum.joinSpace: + return l10n.getStartedCommunitiesComplete; + case OnboardingStepsEnum.inviteFriends: + return l10n.getStartedFriendsComplete; + } + } + + Widget icon(double size) { + switch (this) { + case OnboardingStepsEnum.chatWithBot: + return BotFace(expression: BotExpression.gold, width: size); + case OnboardingStepsEnum.joinSpace: + return Icon(Icons.groups_outlined, size: size); + case OnboardingStepsEnum.inviteFriends: + return Icon(Icons.share, size: size); + } + } + + String buttonText(L10n l10n) { + switch (this) { + case OnboardingStepsEnum.chatWithBot: + return l10n.getStartedBotChatButton; + case OnboardingStepsEnum.joinSpace: + return l10n.findYourPeople; + case OnboardingStepsEnum.inviteFriends: + return l10n.getStartedFriendsButton; + } + } +} diff --git a/lib/pangea/onboarding/onboarding_view.dart b/lib/pangea/onboarding/onboarding_view.dart new file mode 100644 index 000000000..7f383c75c --- /dev/null +++ b/lib/pangea/onboarding/onboarding_view.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_complete.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_constants.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_step.dart'; +import 'package:fluffychat/pangea/onboarding/onboarding_steps_enum.dart'; +import 'package:fluffychat/utils/stream_extension.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class OnboardingView extends StatelessWidget { + final OnboardingController controller; + + const OnboardingView({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + final isColumnMode = FluffyThemes.isColumnMode(context); + + final screenheight = MediaQuery.of(context).size.height; + + return Material( + child: StreamBuilder( + key: ValueKey( + client.userID.toString(), + ), + stream: client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) { + return Stack( + alignment: Alignment.topCenter, + children: [ + if (isColumnMode && !OnboardingController.isClosed) + Positioned( + bottom: 0.0, + child: AnimatedOpacity( + duration: FluffyThemes.animationDuration, + opacity: OnboardingController.isComplete ? 1.0 : 0.3, + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${OnboardingConstants.onboardingImageFileName}", + fit: BoxFit.cover, + ), + ), + ), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + height: OnboardingController.isClosed ? 0 : screenheight, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 12.0, + horizontal: isColumnMode ? 20.0 : 8.0, + ), + child: MaxWidthBody( + showBorder: false, + maxWidth: 850.0, + child: Column( + children: [ + Text( + L10n.of(context).getStarted, + style: TextStyle( + fontSize: isColumnMode ? 32.0 : 16.0, + height: isColumnMode ? 1.2 : 1.5, + ), + ), + Padding( + padding: EdgeInsets.all( + isColumnMode ? 40.0 : 12.0, + ), + child: Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: OnboardingStepsEnum.values.map((step) { + final complete = + OnboardingController.complete(step); + return CircleAvatar( + radius: 6.0, + backgroundColor: complete + ? AppConfig.success + : Theme.of(context).colorScheme.primary, + child: CircleAvatar( + radius: 3.0, + backgroundColor: + Theme.of(context).colorScheme.surface, + ), + ); + }).toList(), + ), + ), + OnboardingController.isComplete + ? OnboardingComplete( + controller: controller, + ) + : Column( + spacing: 12.0, + children: [ + for (final step in OnboardingStepsEnum.values) + OnboardingStep( + step: step, + isComplete: + OnboardingController.complete(step), + onPressed: () => + controller.onPressed(step), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart index 7462caecb..6fd3c1e3b 100644 --- a/lib/widgets/navigation_rail.dart +++ b/lib/widgets/navigation_rail.dart @@ -88,18 +88,10 @@ class SpacesNavigationRail extends StatelessWidget { // #Pangea if (i == 0) { return NaviRailItem( - isSelected: isColumnMode - ? activeSpaceId == null && - !isSettings && - !isCommunities - : isHomepage, + isSelected: isHomepage, onTap: () { - if (isColumnMode) { - onGoToChats(); - } else { - clearActiveSpace?.call(); - context.go("/rooms/homepage"); - } + clearActiveSpace?.call(); + context.go("/rooms/homepage"); }, backgroundColor: Colors.transparent, icon: FutureBuilder( @@ -127,32 +119,30 @@ class SpacesNavigationRail extends StatelessWidget { i--; // Pangea# if (i == 0) { - return isColumnMode - ? const SizedBox() - : NaviRailItem( - // #Pangea - // isSelected: activeSpaceId == null && !isSettings, - isSelected: activeSpaceId == null && - !isSettings && - !isHomepage && - !isCommunities, - // Pangea# - onTap: onGoToChats, - // #Pangea - // icon: const Padding( - // padding: EdgeInsets.all(10.0), - // child: Icon(Icons.forum_outlined), - // ), - // selectedIcon: const Padding( - // padding: EdgeInsets.all(10.0), - // child: Icon(Icons.forum), - // ), - icon: const Icon(Icons.forum_outlined), - selectedIcon: const Icon(Icons.forum), - // Pangea# - toolTip: L10n.of(context).chats, - unreadBadgeFilter: (room) => true, - ); + return NaviRailItem( + // #Pangea + // isSelected: activeSpaceId == null && !isSettings, + isSelected: activeSpaceId == null && + !isSettings && + !isHomepage && + !isCommunities, + // Pangea# + onTap: onGoToChats, + // #Pangea + // icon: const Padding( + // padding: EdgeInsets.all(10.0), + // child: Icon(Icons.forum_outlined), + // ), + // selectedIcon: const Padding( + // padding: EdgeInsets.all(10.0), + // child: Icon(Icons.forum), + // ), + icon: const Icon(Icons.forum_outlined), + selectedIcon: const Icon(Icons.forum), + // Pangea# + toolTip: L10n.of(context).chats, + unreadBadgeFilter: (room) => true, + ); } i--; if (i == rootSpaces.length) {