Merge branch 'main' of https://github.com/pangeachat/client into space-details-tooltips

This commit is contained in:
Kelrap 2025-06-06 14:34:23 -04:00
commit f264d43a6d
23 changed files with 701 additions and 306 deletions

View file

@ -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"
}

View file

@ -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'],

View file

@ -1925,7 +1925,10 @@ class ChatController extends State<ChatPageWithRoom>
return;
}
// Close emoji picker, if open
showEmojiPicker = false;
if (showEmojiPicker) {
hideEmojiPicker();
return;
}
// Check if the user has set their languages. If not, prompt them to do so.
if (!MatrixState.pangeaController.languageController.languagesSet) {

View file

@ -389,6 +389,9 @@ class HtmlMessage extends StatelessWidget {
if (matrixId.sigil == '@') {
final user = room.unsafeGetUserFromMemoryOrFallback(matrixId);
return WidgetSpan(
// #Pangea
alignment: PlaceholderAlignment.middle,
// Pangea#
child: MatrixPill(
key: Key('user_pill_$matrixId'),
name: user.calcDisplayname(),
@ -397,6 +400,9 @@ class HtmlMessage extends StatelessWidget {
outerContext: context,
fontSize: fontSize,
color: linkStyle.color,
// #Pangea
userId: user.id,
// Pangea#
),
);
}
@ -405,6 +411,9 @@ class HtmlMessage extends StatelessWidget {
? this.room.client.getRoomById(matrixId)
: this.room.client.getRoomByAlias(matrixId);
return WidgetSpan(
// #Pangea
alignment: PlaceholderAlignment.middle,
// Pangea#
child: MatrixPill(
name: room?.getLocalizedDisplayname() ?? matrixId,
avatar: room?.avatar,
@ -802,6 +811,9 @@ class MatrixPill extends StatelessWidget {
final String uri;
final double? fontSize;
final Color? color;
// #Pangea
final String? userId;
// Pangea#
const MatrixPill({
super.key,
@ -811,6 +823,9 @@ class MatrixPill extends StatelessWidget {
required this.uri,
required this.fontSize,
required this.color,
// #Pangea
this.userId,
// Pangea#
});
@override
@ -825,6 +840,9 @@ class MatrixPill extends StatelessWidget {
mxContent: avatar,
name: name,
size: 16,
// #Pangea
userId: userId,
// Pangea#
),
const SizedBox(width: 6),
Text(

View file

@ -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#
],
),
);
@ -387,6 +398,7 @@ class _SearchItem extends StatelessWidget {
final void Function() onPressed;
// #Pangea
final BorderRadius? radius;
final String? userId;
// Pangea#
const _SearchItem({
@ -395,6 +407,7 @@ class _SearchItem extends StatelessWidget {
required this.onPressed,
// #Pangea
this.radius,
this.userId,
// Pangea#
});
@ -412,6 +425,7 @@ class _SearchItem extends StatelessWidget {
name: title,
// #Pangea
borderRadius: radius,
userId: userId,
// Pangea#
),
Padding(
@ -467,6 +481,7 @@ class UserSearchResultsListState extends State<UserSearchResultsList> {
widget.userSearchResult.results[i].userId.localpart ??
L10n.of(context).unknownDevice,
avatar: widget.userSearchResult.results[i].avatarUrl,
userId: widget.userSearchResult.results[i].userId,
onPressed: () => UserDialog.show(
context: context,
profile: widget.userSearchResult.results[i],

View file

@ -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),

View file

@ -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,
),

View file

@ -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<SpaceView> {
);
},
),
// #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)),
],
);

View file

@ -206,7 +206,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
),
),
IconButton(
icon: const Icon(Icons.menu_outlined),
icon: const Icon(Icons.event_note_outlined),
onPressed: () => context.go('/rooms/homepage/planner'),
tooltip: L10n.of(context).activityPlannerTitle,
),

View file

@ -213,6 +213,7 @@ class PangeaChatInputRowState extends State<PangeaChatInputRow> {
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: PopupMenuButton<String>(
useRootNavigator: true,
icon: const Icon(Icons.add_outlined),
onSelected: _controller.onAddPopupMenuButtonSelected,
itemBuilder: (BuildContext context) =>

View file

@ -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,
color: theme.colorScheme.onPrimaryContainer,
),
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(),
),
],
),

View file

@ -1,13 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
@ -33,6 +26,11 @@ import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
class PangeaChatDetailsView extends StatelessWidget {
final ChatDetailsController controller;
@ -80,7 +78,7 @@ class PangeaChatDetailsView extends StatelessWidget {
: const Center(child: BackButton())),
),
body: MaxWidthBody(
maxWidth: 800,
maxWidth: 900,
showBorder: false,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
@ -304,8 +302,8 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
super.dispose();
}
final double _buttonWidth = 120.0;
final double _buttonHeight = 70.0;
final double _buttonWidth = 165.0;
final double _buttonHeight = 84.0;
final double _miniButtonWidth = 50.0;
Room get room => widget.room;
@ -315,7 +313,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
return [
ButtonDetails(
title: l10n.activities,
icon: const Icon(Icons.event_note_outlined),
icon: const Icon(Icons.event_note_outlined, size: 30.0),
onPressed: () => context.go("/rooms/${room.id}/details/planner"),
visible: room.canChangeStateEvent(PangeaEventTypes.activityPlan) ||
room.isSpace,
@ -323,14 +321,14 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
),
ButtonDetails(
title: l10n.permissions,
icon: const Icon(Icons.edit_attributes_outlined),
icon: const Icon(Icons.edit_attributes_outlined, size: 30.0),
onPressed: () => context.go('/rooms/${room.id}/details/permissions'),
visible: (room.isRoomAdmin && !room.isDirectChat) || room.isSpace,
enabled: room.isRoomAdmin && !room.isDirectChat,
),
ButtonDetails(
title: l10n.access,
icon: const Icon(Icons.shield_outlined),
icon: const Icon(Icons.shield_outlined, size: 30.0),
onPressed: () => context.go('/rooms/${room.id}/details/access'),
visible: room.isSpace && room.spaceParents.isEmpty,
enabled: room.isSpace && room.isRoomAdmin,
@ -343,6 +341,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
room.pushRuleState == PushRuleState.notify
? Icons.notifications_on_outlined
: Icons.notifications_off_outlined,
size: 30.0,
),
onPressed: () => showFutureLoadingDialog(
context: context,
@ -356,14 +355,14 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
),
ButtonDetails(
title: l10n.invite,
icon: const Icon(Icons.person_add_outlined),
icon: const Icon(Icons.person_add_outlined, size: 30.0),
onPressed: () => context.go('/rooms/${room.id}/details/invite'),
visible: (room.canInvite && !room.isDirectChat) || room.isSpace,
enabled: room.canInvite && !room.isDirectChat,
),
ButtonDetails(
title: l10n.addSubspace,
icon: const Icon(Icons.add_outlined),
icon: const Icon(Icons.add_outlined, size: 30.0),
onPressed: widget.controller.addSubspace,
visible: room.isSpace &&
room.canChangeStateEvent(
@ -373,7 +372,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
),
ButtonDetails(
title: l10n.downloadSpaceAnalytics,
icon: const Icon(Icons.download_outlined),
icon: const Icon(Icons.download_outlined, size: 30.0),
onPressed: () {
showDialog(
context: context,
@ -385,7 +384,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
),
ButtonDetails(
title: l10n.download,
icon: const Icon(Icons.download_outlined),
icon: const Icon(Icons.download_outlined, size: 30.0),
onPressed: widget.controller.downloadChatAction,
visible: room.ownPowerLevel >= 50 && !room.isSpace,
),
@ -406,14 +405,14 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
),
ButtonDetails(
title: l10n.chatCapacity,
icon: const Icon(Icons.reduce_capacity),
icon: const Icon(Icons.reduce_capacity, size: 30.0),
onPressed: widget.controller.setRoomCapacity,
visible:
!room.isSpace && !room.isDirectChat && room.canSendDefaultStates,
),
ButtonDetails(
title: l10n.leave,
icon: const Icon(Icons.logout_outlined),
icon: const Icon(Icons.logout_outlined, size: 30.0),
onPressed: () async {
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
@ -440,7 +439,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
),
ButtonDetails(
title: l10n.delete,
icon: const Icon(Icons.delete_outline),
icon: const Icon(Icons.delete_outline, size: 30.0),
onPressed: () async {
if (room.isSpace) {
final resp = await showDialog<bool?>(
@ -499,6 +498,11 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
final mini = fullButtonCapacity < 4;
final capacity = mini ? miniButtonCapacity : fullButtonCapacity;
debugPrint(
"RoomDetailsButtonRow: $fullButtonCapacity buttons available",
);
debugPrint("Available width: $availableWidth");
List<ButtonDetails> mainViewButtons =
buttons.where((button) => button.showInMainView).toList();
final List<ButtonDetails> otherButtons =
@ -518,6 +522,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
}
return PopupMenuButton(
useRootNavigator: true,
onSelected: (button) => button.onPressed?.call(),
itemBuilder: (context) {
return otherButtons
@ -596,46 +601,44 @@ class RoomDetailsButton extends StatelessWidget {
return const SizedBox();
}
return Tooltip(
message: buttonDetails.title,
child: AbsorbPointer(
absorbing: !buttonDetails.enabled,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: HoverBuilder(
builder: (context, hovered) {
return GestureDetector(
onTap: buttonDetails.onPressed,
child: Opacity(
opacity: buttonDetails.enabled ? 1.0 : 0.5,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: hovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8.0),
child: mini
? buttonDetails.icon
: Column(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
buttonDetails.icon,
Text(
buttonDetails.title,
textAlign: TextAlign.center,
),
],
),
return AbsorbPointer(
absorbing: !buttonDetails.enabled,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: HoverBuilder(
builder: (context, hovered) {
return GestureDetector(
onTap: buttonDetails.onPressed,
child: Opacity(
opacity: buttonDetails.enabled ? 1.0 : 0.5,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: hovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12.0),
child: mini
? buttonDetails.icon
: Column(
spacing: 12.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
buttonDetails.icon,
Text(
buttonDetails.title,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12.0),
),
],
),
),
);
},
),
),
);
},
),
),
);
@ -668,7 +671,7 @@ class RoomParticipantsSection extends StatelessWidget {
super.key,
});
final double _width = 80.0;
final double _width = 100.0;
final double _padding = 12.0;
double get _fullWidth => _width + (_padding * 2);

View file

@ -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<Room> 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);

View file

@ -126,7 +126,8 @@ class FindYourPeopleView extends StatelessWidget {
: BorderSide.none,
borderRadius: BorderRadius.circular(100),
),
contentPadding: EdgeInsets.zero,
contentPadding:
const EdgeInsets.fromLTRB(0, 0, 20.0, 0),
hintText: L10n.of(context).findYourPeople,
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,

View file

@ -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<Onboarding> {
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<void> closeCompletedMessage() async {
await _onboardingStorage.write('closed', true);
if (mounted) setState(() {});
}
Future<void> inviteFriends() async {
FluffyShare.shareInviteLink(context);
await _onboardingStorage.write('invite_friends', true);
if (mounted) setState(() {});
}
Future<void> startChatWithBot() async {
final resp = await showFutureLoadingDialog<String>(
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<void> 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);
}

View file

@ -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,
),
),
],
);
}
}

View file

@ -0,0 +1,3 @@
class OnboardingConstants {
static String onboardingImageFileName = "Getting+Started.png";
}

View file

@ -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,
),
),
],
),
),
],
),
),
],
),
);
}
}

View file

@ -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;
}
}
}

View file

@ -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),
),
],
),
],
),
),
),
),
],
);
},
),
);
}
}

View file

@ -283,7 +283,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
);
double get _columnWidth => FluffyThemes.isColumnMode(context)
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth)
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth + 1.0)
: 0;
/// Available vertical space not taken up by the header and footer

View file

@ -80,8 +80,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
super.initState();
_onPlayerStateChanged = _audioPlayer.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
_audioPlayer.stop();
_audioPlayer.seek(null);
_updateMode(null);
}
setState(() {});
});
@ -121,8 +120,18 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
}
}
Future<void> _updateMode(SelectMode mode) async {
Future<void> _updateMode(SelectMode? mode) async {
_clear();
if (mode == null) {
setState(() {
_audioPlayer.stop();
_audioPlayer.seek(null);
_selectedMode = null;
});
return;
}
setState(
() => _selectedMode =
_selectedMode == mode && mode != SelectMode.audio ? null : mode,

View file

@ -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<Profile>(
@ -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) {