Merge branch 'main' into 2840-show-all-lemma-emojis

This commit is contained in:
wcjord 2025-06-09 16:37:54 -04:00 committed by GitHub
commit 15d9df3966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
96 changed files with 2551 additions and 1958 deletions

View file

@ -17,3 +17,33 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
- name: Set project ID
run: |
echo "PROJECT_ID=PVT_kwDOBndSo84A7FWL" >> $GITHUB_ENV
- name: Get item ID for issue in project
id: get_item_id
run: |
ITEM_ID=$(gh api graphql -f query='query { repository(owner: "${{ github.repository_owner }}", name: "${{ github.event.repository.name }}") { issue(number: ${{ github.event.issue.number }}) { projectItems(first: 10) { nodes { id project { id } } } } }' --jq '.data.repository.issue.projectItems.nodes[] | select(.project.id==env.PROJECT_ID) | .id')
echo "ITEM_ID=$ITEM_ID" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get status field and Done option IDs
id: get_status_ids
run: |
STATUS_FIELD_ID=$(gh api graphql -f query='query { node(id: "'$PROJECT_ID'") { ... on ProjectV2 { fields(first: 20) { nodes { id name } } } } }' --jq '.data.node.fields.nodes[] | select(.name=="Status") | .id')
DONE_OPTION_ID=$(gh api graphql -f query='query { node(id: "'$STATUS_FIELD_ID'") { ... on ProjectV2Field { options { id name } } } }' --jq '.data.node.options[] | select(.name=="Done") | .id')
echo "STATUS_FIELD_ID=$STATUS_FIELD_ID" >> $GITHUB_ENV
echo "DONE_OPTION_ID=$DONE_OPTION_ID" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set status to Done in project
run: |
gh api graphql -f query='mutation($project:ID!, $item:ID!, $field:ID!, $option:ID!) { updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $field, value: { singleSelectOptionId: $option } }) { projectV2Item { id } } }' -f project=$PROJECT_ID -f item=$ITEM_ID -f field=$STATUS_FIELD_ID -f option=$DONE_OPTION_ID
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROJECT_ID: ${{ env.PROJECT_ID }}
ITEM_ID: ${{ env.ITEM_ID }}
STATUS_FIELD_ID: ${{ env.STATUS_FIELD_ID }}
DONE_OPTION_ID: ${{ env.DONE_OPTION_ID }}
# To get your project, field, and option IDs, see the instructions in the new issue_opened_project.yaml file.
# You must replace the placeholders with your actual project and field IDs.

View file

@ -0,0 +1,20 @@
# Auto-add new issues to a GitHub project (replace PROJECT_ID and COLUMN_ID with your values)
name: Add new issues to project
on:
issues:
types:
- opened
jobs:
add_to_project:
runs-on: ubuntu-latest
steps:
- name: Set project ID
run: |
echo "PROJECT_ID=PVT_kwDOBndSo84A7FWL" >> $GITHUB_ENV
- name: Add issue to project
run: |
gh api graphql -f query='mutation($project:ID!, $contentId:ID!) { addProjectV2ItemById(input: {projectId: $project, contentId: $contentId}) { item { id } } }' -f project=$PROJECT_ID -f contentId=$ISSUE_ID
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_ID: ${{ github.event.issue.node_id }}
# To get your project ID, use: gh api graphql -f query='query { organization(login: "<ORG>") { projectV2(number: <PROJECT_NUMBER>) { id } } }'

View file

@ -30,7 +30,7 @@
- chore: Use Cupertino Activity Indicator in ChatEventList (krille-chan)
- chore: Use other join endpoint for room upgrades (Krille)
- fix(macos): update dependencies to make the build work (Rafał Hirsch)
- fix: Add missing <s> html tag to render (Krille)
- fix: Add missing \<s> html tag to render (Krille)
- fix: Consistent element padding between server picker and login view (xegim)
- fix: Index of numbered lists are off (Krille)
- fix: never use a transition on the shell route (Rafał Hirsch)

View file

@ -705,6 +705,15 @@
}
}
},
"countInvited": "{count} invited",
"@countInvited": {
"type": "String",
"placeholders": {
"count": {
"type": "int"
}
}
},
"create": "Create",
"@create": {
"type": "String",
@ -3199,6 +3208,18 @@
}
}
},
"sentVoiceMessage": "\uD83C\uDF99\uFE0F {duration} - {sender}",
"@sentVoiceMessage": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
},
"duration": {
"type": "String"
}
}
},
"deletePushRuleCanNotBeUndone": "If you delete this notification setting, this can not be undone.",
"more": "More",
"shareKeysWith": "Share keys with...",
@ -4975,5 +4996,20 @@
"canBeFoundViaInvitation": "\u2022 invitation",
"canBeFoundViaCodeOrLink": "\u2022 code or link",
"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!"
"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",
"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",
"groupChat": "Group Chat",
"directMessage": "Direct Message",
"newDirectMessage": "New direct message"
}

View file

@ -6083,4 +6083,4 @@
"type": "String",
"placeholders": {}
}
}
}

View file

@ -4382,4 +4382,4 @@
"type": "String",
"placeholders": {}
}
}
}

View file

@ -17,8 +17,6 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A10584DF00E2CBE024A7FEB1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F30C00BA233E7CA67AFBED5 /* Pods_Runner.framework */; };
BCFA6E528F0B53B71B652C77 /* Pods_FluffyChat_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B1F89C23F73F2B8E7922A37 /* Pods_FluffyChat_Share.framework */; };
C1005C45261071B5002F4F32 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1005C44261071B5002F4F32 /* ShareViewController.swift */; };
C1005C48261071B5002F4F32 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C1005C46261071B5002F4F32 /* MainInterface.storyboard */; };
C1005C4C261071B5002F4F32 /* FluffyChat Share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = C1005C42261071B5002F4F32 /* FluffyChat Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -62,7 +60,6 @@
/* Begin PBXFileReference section */
09545B0C8C397F94966EA956 /* Pods-FluffyChat Share.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FluffyChat Share.debug.xcconfig"; path = "Target Support Files/Pods-FluffyChat Share/Pods-FluffyChat Share.debug.xcconfig"; sourceTree = "<group>"; };
0BDDCB1746F84339AF1A5F40 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
23120B990D2B5081843FB313 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
@ -81,8 +78,6 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9B1F89C23F73F2B8E7922A37 /* Pods_FluffyChat_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FluffyChat_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9F30C00BA233E7CA67AFBED5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C1005C42261071B5002F4F32 /* FluffyChat Share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "FluffyChat Share.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
C1005C44261071B5002F4F32 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
C1005C47261071B5002F4F32 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };

View file

@ -7,6 +7,7 @@ abstract class AppConfig {
// static String _applicationName = 'FluffyChat';
static String _applicationName = 'Pangea Chat';
// #Pangea
static String get applicationName => _applicationName;
static String? _applicationWelcomeMessage;
@ -14,7 +15,8 @@ abstract class AppConfig {
// #Pangea
// static String _defaultHomeserver = 'matrix.org';
static String get _defaultHomeserver => Environment.synapseURL;
// #Pangea
// Pangea#
static String get defaultHomeserver => _defaultHomeserver;
static double fontSizeFactor = 1;
static const Color chatColor = primaryColor;
@ -22,6 +24,7 @@ abstract class AppConfig {
static const double messageFontSize = 16.0;
static const bool allowOtherHomeservers = true;
static const bool enableRegistration = true;
// #Pangea
static const double toolbarMaxHeight = 250.0;
static const double toolbarMinHeight = 200.0;
static const double toolbarMinWidth = 350.0;
@ -80,6 +83,7 @@ abstract class AppConfig {
// 'https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md';
static String _privacyUrl = "https://www.pangeachat.com/privacy";
//Pangea#
static String get privacyUrl => _privacyUrl;
// #Pangea
// static const String website = 'https://fluffychat.im';
@ -95,19 +99,21 @@ abstract class AppConfig {
// #Pangea
// static const String appOpenUrlScheme = 'im.fluffychat';
static const String appOpenUrlScheme = 'matrix.pangea.chat';
static String _webBaseUrl = 'https://fluffychat.im/web';
// Pangea#
static String _webBaseUrl = 'https://fluffychat.im/web';
static String get webBaseUrl => _webBaseUrl;
//#Pangea
static const String sourceCodeUrl = 'https://gitlab.com/famedly/fluffychat';
static const String sourceCodeUrl =
'https://github.com/krille-chan/fluffychat';
// #Pangea
// static const String supportUrl =
// 'https://gitlab.com/famedly/fluffychat/issues';
// 'https://github.com/krille-chan/fluffychat/issues';
// static const String changelogUrl =
// 'https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md';
static const String supportUrl = 'https://www.pangeachat.com/faqs';
static const String termsOfServiceUrl =
'https://www.pangeachat.com/terms-of-service';
// static const String changelogUrl =
// 'https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md';
//Pangea#
// Pangea#
static final Uri newIssueUrl = Uri(
scheme: 'https',
host: 'github.com',
@ -143,21 +149,9 @@ abstract class AppConfig {
static const String schemePrefix = 'matrix:';
// #Pangea
// static const String pushNotificationsChannelId = 'fluffychat_push';
// static const String pushNotificationsChannelName = 'FluffyChat push channel';
// static const String pushNotificationsChannelDescription =
// 'Push notifications for FluffyChat';
// static const String pushNotificationsAppId = 'chat.fluffy.fluffychat';
// static const String pushNotificationsGatewayUrl =
// 'https://push.fluffychat.im/_matrix/push/v1/notify';
// static const String pushNotificationsPusherFormat = 'event_id_only';
static const String pushNotificationsChannelId = 'pangeachat_push';
static const String pushNotificationsChannelName = 'Pangea Chat push channel';
static const String pushNotificationsChannelDescription =
'Push notifications for Pangea Chat';
static const String pushNotificationsAppId = 'com.talktolearn.chat';
static const String pushNotificationsGatewayUrl =
'https://sygnal.pangea.chat/_matrix/push/v1/notify';
static const String? pushNotificationsPusherFormat = null;
// Pangea#
static const double borderRadius = 18.0;
static const double columnWidth = 360.0;

View file

@ -32,17 +32,19 @@ import 'package:fluffychat/pages/settings_style/settings_style.dart';
import 'package:fluffychat/pangea/activity_generator/activity_generator.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_suggestions/suggestions_page.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people_side_view.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
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';
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
import 'package:fluffychat/pangea/user/pages/find_partner.dart';
import 'package:fluffychat/widgets/config_viewer.dart';
import 'package:fluffychat/widgets/layouts/empty_page.dart';
import 'package:fluffychat/widgets/layouts/two_column_layout.dart';
@ -153,17 +155,11 @@ abstract class AppRoutes {
),
GoRoute(
path: '/join_with_alias',
pageBuilder: (context, state) => Matrix.of(context).client.isLogged()
? chatListShellRouteBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
)
: defaultPageBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
),
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
),
),
GoRoute(
path: '/user_age',
@ -195,8 +191,13 @@ abstract class AppRoutes {
pageBuilder: (context, state, child) => noTransitionPageBuilder(
context,
state,
// #Pangea
// FluffyThemes.isColumnMode(context) &&
// state.fullPath?.startsWith('/rooms/settings') == false
FluffyThemes.isColumnMode(context) &&
state.fullPath?.startsWith('/rooms/settings') == false
state.fullPath?.startsWith('/rooms/settings') == false &&
state.fullPath?.startsWith('/rooms/communities') == false
// Pangea#
? TwoColumnLayout(
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
@ -238,7 +239,7 @@ abstract class AppRoutes {
FluffyThemes.isColumnMode(context)
// #Pangea
// ? const EmptyPage()
? const SuggestionsPage()
? const Onboarding()
// Pangea#
: ChatList(
activeChat: state.pathParameters['roomid'],
@ -302,16 +303,30 @@ abstract class AppRoutes {
redirect: loggedOutRedirect,
),
// #Pangea
GoRoute(
path: 'partner',
pageBuilder: (context, state) => defaultPageBuilder(
ShellRoute(
pageBuilder: (context, state, child) => defaultPageBuilder(
context,
state,
const FindPartner(),
FluffyThemes.isColumnMode(context)
? TwoColumnLayout(
mainView: const FindYourPeopleSideView(),
sideView: child,
dividerColor: Colors.transparent,
)
: child,
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: 'communities',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const FindYourPeople(),
),
),
],
),
// #Pangea
GoRoute(
path: 'homepage',
redirect: loggedOutRedirect,
@ -748,27 +763,5 @@ abstract class AppRoutes {
redirect: loggedOutRedirect,
),
];
static Page chatListShellRouteBuilder(
context,
state,
child,
) =>
noTransitionPageBuilder(
context,
state,
FluffyThemes.isColumnMode(context) &&
state.fullPath?.startsWith('/rooms/settings') == false
? TwoColumnLayout(
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
activeSpaceId: state.uri.queryParameters['spaceId'],
displayNavigationRail:
state.path?.startsWith('/rooms/settings') != true,
),
sideView: child,
)
: child,
);
// Pangea#
}

View file

@ -50,7 +50,10 @@ enum AppSettings<T> {
// Pangea#
pushNotificationsGatewayUrl<String>(
'pushNotificationsGatewayUrl',
'https://push.fluffychat.im/_matrix/push/v1/notify',
// #Pangea
// 'https://push.fluffychat.im/_matrix/push/v1/notify',
'https://sygnal.pangea.chat/_matrix/push/v1/notify',
// Pangea#
),
pushNotificationsPusherFormat<String>(
'pushNotificationsPusherFormat',

View file

@ -7,7 +7,10 @@ import 'app_config.dart';
abstract class FluffyThemes {
static const double columnWidth = 380.0;
static const double navRailWidth = 80.0;
// #Pangea
// static const double navRailWidth = 80.0;
static const double navRailWidth = 72.0;
// Pangea#
static bool isColumnModeByWidth(double width) =>
width > columnWidth * 2 + navRailWidth;

View file

@ -3,6 +3,7 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:device_info_plus/device_info_plus.dart';
@ -26,6 +27,7 @@ import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/message_analytics_feedback.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
@ -514,7 +516,7 @@ class ChatController extends State<ChatPageWithRoom>
// #Pangea
// If fake event was sent, don't animate in the next event.
// It makes the replacement of the fake event jumpy.
if (_fakeEventID != null) {
if (_fakeEventIDs.isNotEmpty) {
animateInEventIndex = null;
return;
}
@ -686,7 +688,6 @@ class ChatController extends State<ChatPageWithRoom>
MatrixState.pAnyState.closeAllOverlays(force: true);
showToolbarStream.close();
stopMediaStream.close();
hideTextController.dispose();
_levelSubscription?.cancel();
_analyticsSubscription?.cancel();
_router.routeInformationProvider.removeListener(_onRouteChanged);
@ -719,10 +720,6 @@ class ChatController extends State<ChatPageWithRoom>
// TextEditingController sendController = TextEditingController();
PangeaTextController get sendController => choreographer.textController;
/// used to obscure text in text field after sending fake message without
/// changing the actual text in the sendController
final TextEditingController hideTextController = TextEditingController();
// #Pangea
void setSendingClient(Client c) {
@ -757,26 +754,47 @@ class ChatController extends State<ChatPageWithRoom>
pangeaEditingEvent = null;
}
String? _fakeEventID;
bool get obscureText => _fakeEventID != null;
final List<String> _fakeEventIDs = [];
bool get obscureText => _fakeEventIDs.isNotEmpty;
/// Add a fake event to the timeline to visually indicate that a message is being sent.
/// Used when tokenizing after message send, specifically because tokenization for some
/// languages takes some time.
void sendFakeMessage() {
String? sendFakeMessage() {
if (sendController.text.trim().isEmpty) return null;
final eventID = room.sendFakeMessage(
text: sendController.text,
inReplyTo: replyEvent,
editEventId: editEvent?.eventId,
);
setState(() => _fakeEventID = eventID);
sendController.setSystemText("", EditType.other);
setState(() => _fakeEventIDs.add(eventID));
// wait for the next event to come through before clearing any fake event,
// to make the replacement look smooth
room.client.onTimelineEvent.stream
.firstWhere((event) => event.content[ModelKey.tempEventId] == eventID)
.then(
(_) => clearFakeEvent(eventID),
);
return eventID;
}
void clearFakeEvent() {
if (_fakeEventID == null) return;
timeline?.events.removeWhere((e) => e.eventId == _fakeEventID);
void clearFakeEvent(String? eventId) {
if (eventId == null) return;
final inTimeline = timeline != null &&
timeline!.events.any(
(e) => e.eventId == eventId,
);
if (!inTimeline) return;
timeline?.events.removeWhere((e) => e.eventId == eventId);
setState(() {
_fakeEventID = null;
_fakeEventIDs.remove(eventId);
});
}
@ -785,20 +803,26 @@ class ChatController extends State<ChatPageWithRoom>
// but for choero, the tx id is generated before the message send.
// Also, adding PangeaMessageData
Future<void> send({
required String message,
PangeaRepresentation? originalSent,
PangeaRepresentation? originalWritten,
PangeaMessageTokens? tokensSent,
PangeaMessageTokens? tokensWritten,
ChoreoRecord? choreo,
String? tempEventId,
}) async {
if (message.trim().isEmpty) return;
// if (sendController.text.trim().isEmpty) return;
// Pangea#
if (sendController.text.trim().isEmpty) return;
_storeInputTimeoutTimer?.cancel();
final prefs = await SharedPreferences.getInstance();
prefs.remove('draft_$roomId');
var parseCommands = true;
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
// #Pangea
// final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(message);
// Pangea#
if (commandMatch != null &&
!sendingClient.commands.keys.contains(commandMatch[1]!.toLowerCase())) {
final l10n = L10n.of(context);
@ -809,7 +833,13 @@ class ChatController extends State<ChatPageWithRoom>
okLabel: l10n.sendAsText,
cancelLabel: l10n.cancel,
);
if (dialogResult == OkCancelResult.cancel) return;
// #Pangea
// if (dialogResult == OkCancelResult.cancel) return;
if (dialogResult == OkCancelResult.cancel) {
clearFakeEvent(tempEventId);
return;
}
// Pangea#
parseCommands = false;
}
@ -821,15 +851,20 @@ class ChatController extends State<ChatPageWithRoom>
// editEventId: editEvent?.eventId,
// parseCommands: parseCommands,
// );
final previousEdit = editEvent;
// wait for the next event to come through before clearing any fake event,
// to make the replacement look smooth
room.client.onTimelineEvent.stream.first.then((_) => clearFakeEvent());
// If the message and the sendController text don't match, it's possible
// that there was a delay in tokenization before send, and the user started
// typing a new message. We don't want to erase that, so only reset the input
// bar text if the message is the same as the sendController text.
if (message == sendController.text) {
sendController.setSystemText("", EditType.other);
}
final previousEdit = editEvent;
room
.pangeaSendTextEvent(
sendController.text,
message,
inReplyTo: replyEvent,
editEventId: editEvent?.eventId,
parseCommands: parseCommands,
@ -838,6 +873,7 @@ class ChatController extends State<ChatPageWithRoom>
tokensSent: tokensSent,
tokensWritten: tokensWritten,
choreo: choreo,
tempEventId: tempEventId,
)
.then(
(String? msgEventId) async {
@ -914,7 +950,7 @@ class ChatController extends State<ChatPageWithRoom>
s: StackTrace.current,
data: {
'roomId': roomId,
'text': sendController.text,
'text': message,
'inReplyTo': replyEvent?.eventId,
'editEventId': editEvent?.eventId,
},
@ -923,7 +959,7 @@ class ChatController extends State<ChatPageWithRoom>
}
},
).catchError((err, s) {
clearFakeEvent();
clearFakeEvent(tempEventId);
if (err is EventTooLarge) {
showAdaptiveDialog(
context: context,
@ -936,22 +972,21 @@ class ChatController extends State<ChatPageWithRoom>
s: s,
data: {
'roomId': roomId,
'text': sendController.text,
'text': message,
'inReplyTo': replyEvent?.eventId,
'editEventId': editEvent?.eventId,
},
);
});
// sendController.value = TextEditingValue(
// text: pendingText,
// selection: const TextSelection.collapsed(offset: 0),
// );
// Pangea#
sendController.value = TextEditingValue(
text: pendingText,
selection: const TextSelection.collapsed(offset: 0),
);
setState(() {
// #Pangea
// sendController.text = pendingText;
sendController.setSystemText(pendingText, EditType.other);
// Pangea#
_inputTextIsEmpty = pendingText.isEmpty;
replyEvent = null;
@ -1898,7 +1933,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

@ -320,7 +320,12 @@ class ChatInputRow extends StatelessWidget {
)
: FloatingActionButton.small(
tooltip: L10n.of(context).send,
onPressed: controller.send,
// #Pangea
// onPressed: controller.send,
onPressed: () => controller.send(
message: controller.sendController.text,
),
// Pangea#
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(

View file

@ -109,44 +109,43 @@ class ChatView extends StatelessWidget {
],
),
];
} else if (!controller.room.isArchived) {
// #Pangea
return [
IconButton(
icon: const Icon(Icons.search_outlined),
tooltip: L10n.of(context).search,
onPressed: () {
context.go('/rooms/${controller.room.id}/search');
},
),
IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: L10n.of(context).chatDetails,
onPressed: () {
if (GoRouterState.of(context).uri.path.endsWith('/details')) {
context.go('/rooms/${controller.room.id}');
} else {
context.go('/rooms/${controller.room.id}/details');
}
},
),
];
// return [
// if (AppConfig.experimentalVoip &&
// Matrix.of(context).voipPlugin != null &&
// controller.room.isDirectChat)
// IconButton(
// onPressed: controller.onPhoneButtonTap,
// icon: const Icon(Icons.call_outlined),
// tooltip: L10n.of(context).placeCall,
// ),
// EncryptionButton(controller.room),
// ChatSettingsPopupMenu(controller.room, true),
// ];
// Pangea#
}
// } else if (!controller.room.isArchived) {
// return [
// if (AppConfig.experimentalVoip &&
// Matrix.of(context).voipPlugin != null &&
// controller.room.isDirectChat)
// IconButton(
// onPressed: controller.onPhoneButtonTap,
// icon: const Icon(Icons.call_outlined),
// tooltip: L10n.of(context).placeCall,
// ),
// EncryptionButton(controller.room),
// ChatSettingsPopupMenu(controller.room, true),
// ];
// }
// return [];
return [
IconButton(
icon: const Icon(Icons.search_outlined),
tooltip: L10n.of(context).search,
onPressed: () {
context.go('/rooms/${controller.room.id}/search');
},
),
IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: L10n.of(context).chatDetails,
onPressed: () {
if (GoRouterState.of(context).uri.path.endsWith('/details')) {
context.go('/rooms/${controller.room.id}');
} else {
context.go('/rooms/${controller.room.id}/details');
}
},
),
];
// Pangea#
return [];
}
@override

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

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
@ -112,17 +113,39 @@ class ImageBubble extends StatelessWidget {
borderRadius: borderRadius,
child: Hero(
tag: event.eventId,
child: MxcImage(
event: event,
width: width,
height: height,
fit: fit,
animated: animated,
isThumbnail: thumbnailOnly,
placeholder: event.messageType == MessageTypes.Sticker
? null
: _buildPlaceholder,
),
// #Pangea
child: event.content['url'] is String &&
!(event.content['url'] as String).startsWith('mxc')
? CachedNetworkImage(
imageUrl: event.content['url'] as String,
width: width,
height: height,
fit: fit,
placeholder: (context, url) => _buildPlaceholder(context),
)
: MxcImage(
event: event,
width: width,
height: height,
fit: fit,
animated: animated,
isThumbnail: thumbnailOnly,
placeholder: event.messageType == MessageTypes.Sticker
? null
: _buildPlaceholder,
),
// child: MxcImage(
// event: event,
// width: width,
// height: height,
// fit: fit,
// animated: animated,
// isThumbnail: thumbnailOnly,
// placeholder: event.messageType == MessageTypes.Sticker
// ? null
// : _buildPlaceholder,
// ),
// Pangea#
),
),
),

View file

@ -450,8 +450,12 @@ class Message extends StatelessWidget {
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
if (event.relationshipType ==
RelationshipTypes.reply)
if ({
RelationshipTypes.reply,
RelationshipTypes.thread,
}.contains(
event.relationshipType,
))
FutureBuilder<Event?>(
future: event.getReplyEvent(
timeline,

View file

@ -101,7 +101,6 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
autoPlay: true,
autoInitialize: true,
);
// #Pangea
_stopVideoSubscription?.cancel();
_stopVideoSubscription =

View file

@ -429,16 +429,7 @@ class InputBar extends StatelessWidget {
direction: VerticalDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
// #Pangea
// if should obscure text (to make it looks that a message has been sent after sending fake message),
// use hideTextController
// controller: controller,
controller:
(controller?.choreographer.chatController.obscureText) ?? false
? controller?.choreographer.chatController.hideTextController
: controller,
// Pangea#
controller: controller,
focusNode: focusNode,
hideOnSelect: false,
debounceDuration: const Duration(milliseconds: 50),
@ -447,14 +438,10 @@ class InputBar extends StatelessWidget {
builder: (context, _, focusNode) {
final textField = TextField(
enableSuggestions: enableAutocorrect,
readOnly: controller != null &&
(controller!.choreographer.isRunningIT ||
controller!.choreographer.chatController.obscureText),
readOnly:
controller != null && (controller!.choreographer.isRunningIT),
autocorrect: enableAutocorrect,
controller:
(controller?.choreographer.chatController.obscureText) ?? false
? controller?.choreographer.chatController.hideTextController
: controller,
controller: controller,
focusNode: focusNode,
contextMenuBuilder: (c, e) => markdownContextBuilder(
c,

View file

@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';

View file

@ -85,14 +85,12 @@ class ParticipantListItem extends StatelessWidget {
),
],
),
subtitle:
// #Pangea
LevelDisplayName(userId: user.id),
// Text(
// #Pangea
subtitle: LevelDisplayName(userId: user.id),
// subtitle: Text(
// user.id,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// Pangea#
leading: Opacity(
opacity: user.membership == Membership.join ? 1 : 0.5,

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 {
@ -41,6 +43,9 @@ class ChatListView extends StatelessWidget {
activeSpaceId: controller.activeSpaceId,
onGoToChats: controller.clearActiveSpace,
onGoToSpaceId: controller.setActiveSpace,
// #Pangea
clearActiveSpace: controller.clearActiveSpace,
// Pangea#
),
Container(
color: Theme.of(context).dividerColor,
@ -56,14 +61,25 @@ 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),
// #Pangea
icon: const Icon(Icons.chat_bubble_outline),
// icon: const Icon(Icons.add_outlined),
// Pangea#
label: Text(
L10n.of(context).chat,
// #Pangea
L10n.of(context).directMessage,
// L10n.of(context).chat,
// Pangea#
overflow: TextOverflow.fade,
),
)

View file

@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -56,28 +57,27 @@ class ClientChooserButton extends StatelessWidget {
],
),
),
// Currently disabled because of:
// https://github.com/matrix-org/matrix-react-sdk/pull/12286
/*PopupMenuItem(
PopupMenuItem(
value: SettingsAction.archive,
child: Row(
children: [
const Icon(Icons.archive_outlined),
const SizedBox(width: 18),
Text(L10n.of(context)!.archive),
],
),
),*/
PopupMenuItem(
value: SettingsAction.settings,
child: Row(
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 18),
Text(L10n.of(context).settings),
Text(L10n.of(context).archive),
],
),
),
if (!FluffyThemes.isColumnMode(context))
PopupMenuItem(
value: SettingsAction.settings,
child: Row(
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 18),
Text(L10n.of(context).settings),
],
),
),
const PopupMenuDivider(),
for (final bundle in bundles) ...[
if (matrix.accountBundles[bundle]!.length != 1 ||
@ -158,30 +158,22 @@ class ClientChooserButton extends StatelessWidget {
matrix.accountBundles.forEach((key, value) => clientCount += value.length);
return FutureBuilder<Profile>(
future: matrix.client.fetchOwnProfile(),
builder: (context, snapshot) => Stack(
alignment: Alignment.center,
children: [
...List.generate(
clientCount,
(index) => const SizedBox.shrink(),
),
const SizedBox.shrink(),
const SizedBox.shrink(),
PopupMenuButton<Object>(
onSelected: (o) => _clientSelected(o, context),
itemBuilder: _bundleMenuItems,
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(99),
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
matrix.client.userID!.localpart,
size: 32,
),
builder: (context, snapshot) => Material(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(99),
color: Colors.transparent,
child: PopupMenuButton<Object>(
onSelected: (o) => _clientSelected(o, context),
itemBuilder: _bundleMenuItems,
child: Center(
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name:
snapshot.data?.displayName ?? matrix.client.userID!.localpart,
size: 32,
),
),
],
),
),
);
}

View file

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:badges/badges.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
import '../../config/themes.dart';
@ -35,14 +34,27 @@ class NaviRailItem extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final borderRadius = BorderRadius.circular(AppConfig.borderRadius);
// #Pangea
// final borderRadius = BorderRadius.circular(AppConfig.borderRadius);
final borderRadius = BorderRadius.circular(10.0);
final isColumnMode = FluffyThemes.isColumnMode(context);
final width = isColumnMode
? FluffyThemes.navRailWidth
: FluffyThemes.navRailWidth - 8.0;
// Pangea#
final icon = isSelected ? selectedIcon ?? this.icon : this.icon;
final unreadBadgeFilter = this.unreadBadgeFilter;
return HoverBuilder(
builder: (context, hovered) {
// #Pangea
// return SizedBox(
// height: 72,
return SizedBox(
height: 72,
width: FluffyThemes.navRailWidth,
height: width - (isColumnMode ? 16.0 : 12.0),
width: width,
// width: FluffyThemes.navRailWidth,
// Pangea#
child: Stack(
children: [
Positioned(
@ -53,7 +65,7 @@ class NaviRailItem extends StatelessWidget {
// #Pangea
// width: isSelected ? 8 : 0,
width: isSelected
? FluffyThemes.isColumnMode(context)
? isColumnMode
? 8
: 4
: 0,
@ -74,16 +86,25 @@ class NaviRailItem extends StatelessWidget {
scale: hovered ? 1.1 : 1.0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: Material(
borderRadius: borderRadius,
// #Pangea
// color: isSelected
// ? theme.colorScheme.primaryContainer
// : theme.colorScheme.surfaceContainerHigh,
color: backgroundColor ??
(isSelected
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerHigh),
// #Pangea
// child: Material(
// borderRadius: borderRadius,
// color: isSelected
// ? theme.colorScheme.primaryContainer
// : theme.colorScheme.surfaceContainerHigh,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: backgroundColor ??
(isSelected
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerHigh),
borderRadius: borderRadius,
),
margin: EdgeInsets.symmetric(
horizontal: isColumnMode ? 16.0 : 12.0,
vertical: isColumnMode ? 8.0 : 6.0,
),
// Pangea#
child: Tooltip(
message: toolTip,
@ -95,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,9 +17,11 @@ 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';
import 'package:fluffychat/pangea/spaces/widgets/leaderboard_participant_list.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
@ -176,13 +178,17 @@ class _SpaceViewState extends State<SpaceView> {
await _joinDefaultChats();
} catch (e, s) {
Logs().w('Unable to load hierarchy', e, s);
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toLocalizedString(context))),
);
}
} finally {
setState(() {
_isLoading = false;
});
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@ -680,7 +686,7 @@ class _SpaceViewState extends State<SpaceView> {
// label: Text(L10n.of(context).group),
onPressed: () =>
context.go("/rooms/newgroup?space=${widget.spaceId}"),
label: Text(L10n.of(context).chat),
label: Text(L10n.of(context).groupChat),
// Pangea#
icon: const Icon(Icons.group_add_outlined),
)
@ -802,6 +808,14 @@ class _SpaceViewState extends State<SpaceView> {
// },
// ),
KnockingUsersIndicator(room: room),
SliverList.builder(
itemCount: 1,
itemBuilder: (context, i) {
return LeaderboardParticipantList(
space: room,
);
},
),
// Pangea#
SliverList.builder(
itemCount: joinedRooms.length,
@ -943,6 +957,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

@ -98,11 +98,21 @@ class PresenceAvatar extends StatelessWidget {
final CachedPresence presence;
final double height;
final void Function(Profile) onTap;
// #Pangea
final LinearGradient? gradient;
final Widget? floatingIndicator;
final bool showPresence;
// Pangea#
const PresenceAvatar({
required this.presence,
required this.height,
required this.onTap,
// #Pangea
this.gradient,
this.showPresence = true,
this.floatingIndicator,
// Pangea#
super.key,
});
@ -146,7 +156,11 @@ class PresenceAvatar extends StatelessWidget {
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
gradient: presence.gradient,
// #Pangea
// gradient: presence.gradient,
gradient: gradient ??
(showPresence ? presence.gradient : null),
// Pangea#
borderRadius:
BorderRadius.circular(avatarSize),
),
@ -159,7 +173,11 @@ class PresenceAvatar extends StatelessWidget {
size: avatarSize - 6,
),
),
if (presence.userid == client.userID)
// #Pangea
// if (presence.userid == client.userID)
if (floatingIndicator == null &&
presence.userid == client.userID)
// Pangea#
Positioned(
right: 0,
bottom: 0,
@ -182,6 +200,9 @@ class PresenceAvatar extends StatelessWidget {
),
),
),
// #Pangea
if (floatingIndicator != null) floatingIndicator!,
// Pangea#
if (statusMsg != null) ...[
Positioned(
left: 0,

View file

@ -34,11 +34,7 @@ class ChatMembersController extends State<ChatMembersPage> {
final members = this
.members
?.where(
(member) =>
membershipFilter == Membership.join ||
member.membership == membershipFilter,
)
?.where((member) => member.membership == membershipFilter)
.toList();
if (filter.isEmpty) {

View file

@ -144,9 +144,30 @@ class ChatMembersView extends StatelessWidget {
Membership.ban =>
L10n.of(context).banned,
Membership.invite =>
L10n.of(context).invited,
L10n.of(context).countInvited(
room.summary
.mInvitedMemberCount ??
controller.members
?.where(
(member) =>
member.membership ==
Membership.invite,
)
.length ??
0,
),
Membership.join =>
L10n.of(context).all,
L10n.of(context).countParticipants(
room.summary.mJoinedMemberCount ??
controller.members
?.where(
(member) =>
member.membership ==
Membership.join,
)
.length ??
0,
),
Membership.knock =>
L10n.of(context).knocking,
Membership.leave =>

View file

@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings_view.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/permission_slider_dialog.dart';
@ -71,6 +72,59 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
false),
);
// #Pangea
Map<String, dynamic> get defaultPowerLevels {
final chatPowerLevels = RoomDefaults.defaultPowerLevels(
Matrix.of(context).client.userID!,
).content;
final spacePowerLevels = RoomDefaults.defaultSpacePowerLevels(
Matrix.of(context).client.userID!,
).content;
if (roomId == null) return chatPowerLevels;
final room = Matrix.of(context).client.getRoomById(roomId!);
if (room == null) return chatPowerLevels;
return room.isSpace ? spacePowerLevels : chatPowerLevels;
}
int getDefaultValue(
String permissionKey, {
String? category,
}) {
final room = Matrix.of(context).client.getRoomById(roomId!);
if (room == null) return 0;
final powerLevelsContent = Map<String, Object?>.from(
room.getState(EventTypes.RoomPowerLevels)?.content ?? {},
);
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
..removeWhere((k, v) => v is! int);
if (category == null) {
switch (permissionKey) {
case 'users_default':
case 'events_default':
return powerLevels[permissionKey] ?? 0;
case 'state_default':
return powerLevels[permissionKey] ?? 50;
case 'ban':
case 'kick':
case 'invite':
return powerLevels[permissionKey] ?? 0;
case 'redact':
return powerLevels[permissionKey] ??
powerLevels['events_default'] ??
0;
}
} else if (category == 'events') {
return room.powerForChangingStateEvent(permissionKey);
}
return 0;
}
// Pangea#
@override
Widget build(BuildContext context) => ChatPermissionsSettingsView(this);
}

View file

@ -44,6 +44,35 @@ class ChatPermissionsSettingsView extends StatelessWidget {
final eventsPowerLevels = Map<String, int?>.from(
powerLevelsContent.tryGetMap<String, int?>('events') ?? {},
)..removeWhere((k, v) => v is! int);
// #Pangea
final defaults = Map<String, dynamic>.from(
controller.defaultPowerLevels,
);
Map<String, dynamic> missingPowerLevels = Map<String, dynamic>.from(
defaults,
)..removeWhere((k, v) => v is! int || powerLevels.containsKey(k));
missingPowerLevels = missingPowerLevels.map(
(key, value) => MapEntry(key, controller.getDefaultValue(key)),
);
Map<String, int?> missingEventsPowerLevels = Map<String, int?>.from(
defaults.tryGetMap<String, int?>('events') ?? {},
)..removeWhere(
(k, v) => v is! int || eventsPowerLevels.containsKey(k),
);
missingEventsPowerLevels = missingEventsPowerLevels.map(
(key, value) => MapEntry(
key,
controller.getDefaultValue(key, category: 'events'),
),
);
powerLevels.addAll(missingPowerLevels);
eventsPowerLevels.addAll(missingEventsPowerLevels);
// Pangea#
return Column(
children: [
ListTile(

View file

@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
class PermissionsListTile extends StatelessWidget {
final String permissionKey;
@ -100,6 +101,8 @@ class PermissionsListTile extends StatelessWidget {
return L10n.of(context).pinMessages;
case EventTypes.RoomJoinRules:
return L10n.of(context).setJoinRules;
case PangeaEventTypes.activityPlan:
return L10n.of(context).sendActivities;
// Pangea#
}
}

View file

@ -30,7 +30,10 @@ class NewPrivateChatView extends StatelessWidget {
appBar: AppBar(
scrolledUnderElevation: 0,
leading: const Center(child: BackButton()),
title: Text(L10n.of(context).newChat),
// #Pangea
title: Text(L10n.of(context).newDirectMessage),
// title: Text(L10n.of(context).newChat),
// Pangea#
backgroundColor: theme.scaffoldBackgroundColor,
// #Pangea
// actions: [
@ -125,7 +128,7 @@ class NewPrivateChatView extends StatelessWidget {
),
style: TextStyle(
color: theme.colorScheme.onSurface,
fontSize: 13,
fontSize: 12,
),
),
),
@ -167,9 +170,15 @@ class NewPrivateChatView extends StatelessWidget {
vertical: 24.0,
),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
color: theme.colorScheme.primaryContainer,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
side: BorderSide(
width: 3,
color: theme.colorScheme.primary,
),
),
color: Colors.transparent,
clipBehavior: Clip.hardEdge,
child: InkWell(
borderRadius:
@ -179,10 +188,10 @@ class NewPrivateChatView extends StatelessWidget {
userId,
),
child: Padding(
padding: const EdgeInsets.all(32.0),
padding: const EdgeInsets.all(16.0),
child: ConstrainedBox(
constraints:
const BoxConstraints(maxWidth: 256),
const BoxConstraints(maxWidth: 200),
child: PrettyQrView.data(
// #Pangea
// data: 'https://matrix.to/#/$userId',
@ -191,8 +200,7 @@ class NewPrivateChatView extends StatelessWidget {
decoration: PrettyQrDecoration(
shape: PrettyQrSmoothSymbol(
roundFactor: 1,
color:
theme.colorScheme.onPrimaryContainer,
color: theme.colorScheme.primary,
),
),
),

View file

@ -207,6 +207,36 @@ class SettingsController extends State<Settings> {
// Pangea#
}
// #Pangea
void setStatus() async {
final client = Matrix.of(context).client;
final currentPresence = await client.fetchCurrentPresence(client.userID!);
final input = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).setStatus,
message: L10n.of(context).leaveEmptyToClearStatus,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).statusExampleMessage,
maxLines: 6,
minLines: 1,
maxLength: 255,
initialText: currentPresence.statusMsg,
);
if (input == null) return;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => client.setPresence(
client.userID!,
PresenceType.online,
statusMsg: input,
),
);
}
// Pangea#
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;

View file

@ -165,6 +165,25 @@ class SettingsView extends StatelessWidget {
// style: const TextStyle(fontSize: 12),
),
),
// #Pangea
TextButton.icon(
onPressed: controller.setStatus,
icon: const Icon(
Icons.add,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
theme.colorScheme.secondary,
iconColor: theme.colorScheme.secondary,
),
label: Text(
L10n.of(context).setStatus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// Pangea#
],
),
),

View file

@ -1,5 +1,3 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -25,8 +23,6 @@ class ActivityPlannerBuilder extends StatefulWidget {
final Future<void> Function(
String,
ActivityPlanModel,
Uint8List?,
String?,
)? onEdit;
const ActivityPlannerBuilder({
@ -204,8 +200,6 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
await widget.onEdit!(
widget.initialActivity.bookmarkId,
updatedActivity,
avatar,
filename,
);
}
}
@ -225,7 +219,6 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
updatedActivity,
avatar: avatar,
filename: filename,
avatarURL: imageURL,
);
}

View file

@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
@ -28,16 +29,6 @@ class ActivityPlannerPageAppBar extends StatelessWidget
final theme = Theme.of(context);
return AppBar(
leadingWidth: 150.0,
leading: Row(
children: [
const SizedBox(width: 8.0),
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
],
),
title: pageMode == PageMode.savedActivities
? Row(
mainAxisAlignment: MainAxisAlignment.center,
@ -63,52 +54,59 @@ class ActivityPlannerPageAppBar extends StatelessWidget
],
),
actions: [
Container(
width: 150.0,
alignment: Alignment.center,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => roomID != null
? context.go('/rooms/$roomID/details/planner/generator')
: context.go("/rooms/homepage/planner/generator"),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(36.0),
),
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 10.0,
),
child: Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
CustomizedSvg(
svgUrl:
"${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.crayonIconPath}",
colorReplacements: {
"#CDBEF9": colorToHex(
theme.colorScheme.secondary,
),
},
height: 16.0,
width: 16.0,
),
Flexible(
child: Text(
L10n.of(context).createActivity,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
FluffyThemes.isColumnMode(context)
? Container(
width: 150.0,
alignment: Alignment.center,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => roomID != null
? context.go('/rooms/$roomID/details/planner/generator')
: context.go("/rooms/homepage/planner/generator"),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(36.0),
),
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 10.0,
),
child: Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
CustomizedSvg(
svgUrl:
"${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.crayonIconPath}",
colorReplacements: {
"#CDBEF9": colorToHex(
theme.colorScheme.secondary,
),
},
height: 16.0,
width: 16.0,
),
Flexible(
child: Text(
L10n.of(context).createActivity,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
)
: IconButton(
icon: const Icon(Icons.add),
onPressed: () => roomID != null
? context.go('/rooms/$roomID/details/planner/generator')
: context.go("/rooms/homepage/planner/generator"),
),
),
),
),
],
);
}

View file

@ -1,5 +1,3 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -12,7 +10,6 @@ import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class BookmarkedActivitiesList extends StatefulWidget {
final Room? room;
@ -42,27 +39,7 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
Future<void> _onEdit(
String activityId,
ActivityPlanModel activity,
Uint8List? avatar,
String? filename,
) async {
if (avatar != null) {
final url = await Matrix.of(context).client.uploadContent(
avatar,
filename: filename,
);
if (!mounted) return;
setState(() {
activity = ActivityPlanModel(
req: activity.req,
title: activity.title,
learningObjective: activity.learningObjective,
instructions: activity.instructions,
vocab: activity.vocab,
imageURL: url.toString(),
);
});
}
await BookmarkedActivitiesRepo.remove(activityId);
await BookmarkedActivitiesRepo.save(activity);
if (mounted) setState(() {});

View file

@ -128,7 +128,6 @@ class ActivityRoomSelectionState extends State<ActivityRoomSelection> {
widget.controller.updatedActivity,
avatar: widget.controller.avatar,
filename: widget.controller.filename,
avatarURL: widget.controller.imageURL,
);
_launchStatus[room.id] = 1;
} catch (e, s) {
@ -205,7 +204,6 @@ class ActivityRoomSelectionState extends State<ActivityRoomSelection> {
widget.controller.updatedActivity,
avatar: widget.controller.avatar,
filename: widget.controller.filename,
avatarURL: widget.controller.imageURL,
);
}
_launchStatus["placeholder"] = 1;

View file

@ -114,13 +114,15 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
final resp = await ActivitySearchRepo.get(request).timeout(
const Duration(seconds: 5),
onTimeout: () {
setState(() {
_timeout = true;
_loading = false;
});
if (mounted) {
setState(() {
_timeout = true;
_loading = false;
});
}
Future.delayed(const Duration(seconds: 5), () {
_setActivityItems(retries: retries + 1);
if (mounted) _setActivityItems(retries: retries + 1);
});
return ActivityPlanResponse(activityPlans: []);
},
@ -206,7 +208,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

@ -15,8 +15,9 @@ class SuggestionsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isColumnMode = FluffyThemes.isColumnMode(context);
return Material(
child: SafeArea(
return Scaffold(
resizeToAvoidBottomInset: true,
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View file

@ -132,7 +132,11 @@ extension AnalyticsClientExtension on Client {
/// so other members of the space need to add their analytics rooms to the space.
Future<void> addAnalyticsRoomsToSpaces() async {
if (userID == null || userID == BotName.byEnvironment) return;
final spaces = rooms.where((room) => room.isSpace).toList();
final spaces = rooms
.where(
(room) => room.isSpace && room.membership == Membership.join,
)
.toList();
final Random random = Random();
for (final space in spaces) {

View file

@ -2,14 +2,18 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
class LearningProgressIndicatorButton extends StatelessWidget {
class HoverButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget child;
final BorderRadius? borderRadius;
final double hoverOpacity;
const LearningProgressIndicatorButton({
const HoverButton({
super.key,
required this.onPressed,
required this.child,
this.borderRadius,
this.hoverOpacity = 0.2,
});
@override
@ -23,9 +27,12 @@ class LearningProgressIndicatorButton extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
color: hovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
? Theme.of(context)
.colorScheme
.primary
.withAlpha((hoverOpacity * 255).round())
: Colors.transparent,
borderRadius: BorderRadius.circular(36.0),
borderRadius: borderRadius ?? BorderRadius.circular(36.0),
),
padding: const EdgeInsets.symmetric(
vertical: 2.0,

View file

@ -105,7 +105,7 @@ class LearningProgressIndicatorsState
spacing: 16.0,
children: ConstructTypeEnum.values
.map(
(c) => LearningProgressIndicatorButton(
(c) => HoverButton(
onPressed: () {
showDialog<AnalyticsPopupWrapper>(
context: context,
@ -124,7 +124,7 @@ class LearningProgressIndicatorsState
.toList(),
),
),
LearningProgressIndicatorButton(
HoverButton(
onPressed: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),

View file

@ -1,5 +1,7 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
class RoomDefaults {
static StateEvent defaultPowerLevels(String userID) => StateEvent(
type: EventTypes.RoomPowerLevels,
@ -10,6 +12,7 @@ class RoomDefaults {
"invite": 50,
"redact": 50,
"events": {
PangeaEventTypes.activityPlan: 0,
"m.room.power_levels": 100,
"m.room.pinned_events": 50,
},
@ -34,6 +37,7 @@ class RoomDefaults {
"invite": 50,
"redact": 50,
"events": {
PangeaEventTypes.activityPlan: 50,
"m.room.power_levels": 100,
"m.room.pinned_events": 50,
},

View file

@ -73,7 +73,10 @@ class ChatInputBarState extends State<ChatInputBar> {
),
child: Column(
children: [
ReplyDisplay(widget.controller),
// #Pangea
if (!widget.controller.obscureText)
// Pangea#
ReplyDisplay(widget.controller),
PangeaChatInputRow(
controller: widget.controller,
),

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

@ -3,7 +3,6 @@ import 'package:flutter/material.dart' hide Visibility;
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/pages/chat_access_settings/chat_access_settings_controller.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
@ -168,20 +167,20 @@ class ChatAccessTile extends StatelessWidget {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
borderRadius: BorderRadius.circular(10),
child: Opacity(
opacity: selected ? 1.0 : 0.5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: selected
? theme.colorScheme.primaryContainer
: theme.colorScheme.outline,
? theme.colorScheme.primaryFixedDim
: theme.colorScheme.surfaceContainerHigh,
width: 2,
),
color: selected
? theme.colorScheme.primaryContainer.withAlpha(50)
? theme.colorScheme.primaryFixedDim.withAlpha(50)
: null,
),
padding: const EdgeInsets.all(16.0),

View file

@ -8,7 +8,6 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.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';
@ -19,19 +18,21 @@ import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/conversation_bot/conversation_bot_settings.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/pangea/spaces/widgets/download_space_analytics_dialog.dart';
import 'package:fluffychat/pangea/spaces/widgets/leaderboard_participant_list.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
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';
class PangeaChatDetailsView extends StatelessWidget {
final ChatDetailsController controller;
@ -79,7 +80,7 @@ class PangeaChatDetailsView extends StatelessWidget {
: const Center(child: BackButton())),
),
body: MaxWidthBody(
maxWidth: 800,
maxWidth: 900,
showBorder: false,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
@ -303,8 +304,8 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
super.dispose();
}
final double _buttonWidth = 120.0;
final double _buttonHeight = 70.0;
final double _buttonWidth = 125.0;
final double _buttonHeight = 84.0;
final double _miniButtonWidth = 50.0;
Room get room => widget.room;
@ -314,21 +315,22 @@ 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.canSendDefaultStates || room.isSpace,
enabled: room.canSendDefaultStates,
visible: room.canChangeStateEvent(PangeaEventTypes.activityPlan) ||
room.isSpace,
enabled: room.canChangeStateEvent(PangeaEventTypes.activityPlan),
),
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,
@ -341,6 +343,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,
@ -354,14 +357,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(
@ -371,7 +374,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,
@ -383,7 +386,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,
),
@ -404,14 +407,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,
@ -438,7 +441,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?>(
@ -516,6 +519,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
}
return PopupMenuButton(
useRootNavigator: true,
onSelected: (button) => button.onPressed?.call(),
itemBuilder: (context) {
return otherButtons
@ -594,43 +598,53 @@ class RoomDetailsButton extends StatelessWidget {
return const SizedBox();
}
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(8.0),
child: mini
? buttonDetails.icon
: Column(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
buttonDetails.icon,
Text(
buttonDetails.title,
textAlign: TextAlign.center,
return TooltipVisibility(
visible: mini,
child: 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: EdgeInsets.all(mini ? 6 : 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),
),
],
),
],
),
),
),
);
},
),
),
);
},
),
),
),
),
);
@ -663,7 +677,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);
@ -680,60 +694,54 @@ class RoomParticipantsSection extends StatelessWidget {
builder: (context, constraints) {
final availableWidth = constraints.maxWidth;
final capacity = (availableWidth / _fullWidth).floor();
if (capacity < 4) {
return Column(
children: [
...members.map((member) => ParticipantListItem(member)),
if (actualMembersCount - members.length > 0)
ListTile(
title: Text(
L10n.of(context).loadCountMoreParticipants(
(actualMembersCount - members.length),
),
),
leading: CircleAvatar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
child: const Icon(
Icons.group_outlined,
color: Colors.grey,
),
),
onTap: () => context.push(
'/rooms/${room.id}/details/members',
),
trailing: const Icon(Icons.chevron_right_outlined),
),
],
);
}
return LoadParticipantsUtil(
space: room,
builder: (participantsLoader) {
if (capacity < 4) {
return Column(
children: [
...members.map((member) => ParticipantListItem(member)),
if (actualMembersCount - members.length > 0)
ListTile(
title: Text(
L10n.of(context).loadCountMoreParticipants(
(actualMembersCount - members.length),
),
),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
child: const Icon(
Icons.group_outlined,
color: Colors.grey,
),
),
onTap: () => context.push(
'/rooms/${room.id}/details/members',
),
trailing: const Icon(Icons.chevron_right_outlined),
),
],
);
}
final filteredParticipants =
participantsLoader.filteredParticipants("");
return Wrap(
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
children: [
...filteredParticipants.mapIndexed((index, user) {
Color? color = index == 0
? AppConfig.gold
: index == 1
? Colors.grey[400]!
: index == 2
? Colors.brown[400]!
: null;
final publicProfile = participantsLoader.getPublicProfile(
user.id,
);
LinearGradient? gradient = index.leaderboardGradient;
if (user.id == BotName.byEnvironment ||
publicProfile == null ||
publicProfile.level == null) {
color = null;
gradient = null;
}
return Padding(
@ -745,21 +753,13 @@ class RoomParticipantsSection extends StatelessWidget {
Stack(
alignment: Alignment.center,
children: [
if (color != null)
if (gradient != null)
CircleAvatar(
radius: _width / 2,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: const Alignment(0.5, -0.5),
end: const Alignment(-0.5, 0.5),
colors: <Color>[
color,
Colors.white,
color,
],
),
gradient: gradient,
),
),
)
@ -768,27 +768,27 @@ class RoomParticipantsSection extends StatelessWidget {
height: _width,
width: _width,
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => UserDialog.show(
context: context,
profile: Profile(
userId: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
Builder(
builder: (context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => showMemberActionsPopupMenu(
context: context,
user: user,
),
child: Center(
child: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
size: _width - 6.0,
presenceUserId: user.id,
showPresence: false,
),
),
),
),
child: Center(
child: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
size: _width - 6.0,
presenceUserId: user.id,
showPresence: false,
),
),
),
);
},
),
],
),

View file

@ -114,7 +114,9 @@ class Choreographer {
maxWidth: 325,
transformTargetId: inputTransformTargetKey,
)
: chatController.send();
: chatController.send(
message: chatController.sendController.text,
);
return;
}
@ -135,7 +137,12 @@ class Choreographer {
return;
}
chatController.sendFakeMessage();
if (chatController.sendController.text.trim().isEmpty) {
return;
}
final message = chatController.sendController.text;
final fakeEventId = chatController.sendFakeMessage();
final PangeaRepresentation? originalWritten =
choreoRecord.includedIT && itController.sourceText != null
? PangeaRepresentation(
@ -156,7 +163,7 @@ class Choreographer {
repEventId: null,
room: chatController.room,
req: TokensRequestModel(
fullText: currentText,
fullText: message,
senderL1: l1LangCode!,
senderL2: l2LangCode!,
),
@ -167,7 +174,7 @@ class Choreographer {
originalSent = PangeaRepresentation(
langCode: res?.detections.firstOrNull?.langCode ??
LanguageKeys.unknownLanguage,
text: currentText,
text: message,
originalSent: true,
originalWritten: originalWritten == null,
);
@ -183,7 +190,7 @@ class Choreographer {
e: e,
s: s,
data: {
"currentText": currentText,
"currentText": message,
"l1LangCode": l1LangCode,
"l2LangCode": l2LangCode,
"choreoRecord": choreoRecord.toJson(),
@ -191,9 +198,11 @@ class Choreographer {
);
} finally {
chatController.send(
message: message,
originalSent: originalSent,
tokensSent: tokensSent,
choreo: choreoRecord,
tempEventId: fakeEventId,
);
clear();
}
@ -558,8 +567,6 @@ class Choreographer {
choreoRecord = ChoreoRecord.newRecord;
itController.clear();
igc.dispose();
//@ggurdin - why is this commented out?
// errorService.clear();
_resetDebounceTimer();
}

View file

@ -121,12 +121,7 @@ class Environment {
try {
final String jsonString = await rootBundle.loadString('envs.json');
data = jsonDecode(jsonString);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {},
);
} catch (e) {
return [];
}

View file

@ -90,6 +90,7 @@ class ModelKey {
static const String messageTagMorphEdit = "morph_edit";
static const String messageTagLemmaEdit = "lemma_edit";
static const String messageTagActivityPlan = "activity_plan";
static const String tempEventId = "temporary_event_id";
static const String baseDefinition = "base_definition";
static const String targetDefinition = "target_definition";

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

@ -97,7 +97,10 @@ class MessageDataController extends BaseController {
repEventId: repEventId,
req: req,
room: room,
);
).catchError((e, s) {
_tokensCache.remove(req.hashCode);
return Future<TokensResponseModel>.error(e, s);
});
/////// translation ////////

View file

@ -202,6 +202,7 @@ extension EventsRoomExtension on Room {
PangeaMessageTokens? tokensWritten,
ChoreoRecord? choreo,
String? messageTag,
String? tempEventId,
}) {
// if (parseCommands) {
// return client.parseAndRunCommand(this, message,
@ -233,6 +234,9 @@ extension EventsRoomExtension on Room {
if (messageTag != null) {
event[ModelKey.messageTags] = messageTag;
}
if (tempEventId != null) {
event[ModelKey.tempEventId] = tempEventId;
}
if (parseMarkdown) {
final html = markdown(
@ -269,15 +273,21 @@ extension EventsRoomExtension on Room {
Future<void> sendActivityPlan(
ActivityPlanModel activity, {
Uint8List? avatar,
String? avatarURL,
String? filename,
}) async {
BookmarkedActivitiesRepo.save(activity);
String? imageURL = activity.imageURL;
final eventId = await pangeaSendTextEvent(
activity.markdown,
messageTag: ModelKey.messageTagActivityPlan,
);
Uint8List? bytes = avatar;
if (avatarURL != null && bytes == null) {
if (imageURL != null && bytes == null) {
try {
final resp = await http
.get(Uri.parse(avatarURL))
.get(Uri.parse(imageURL))
.timeout(const Duration(seconds: 5));
bytes = resp.bodyBytes;
} catch (e, s) {
@ -285,12 +295,20 @@ extension EventsRoomExtension on Room {
e: e,
s: s,
data: {
"avatarURL": avatarURL,
"avatarURL": imageURL,
},
);
}
}
if (bytes != null && imageURL == null) {
final url = await client.uploadContent(
bytes,
filename: filename,
);
imageURL = url.toString();
}
MatrixFile? file;
if (filename != null && bytes != null) {
file = MatrixFile(
@ -298,19 +316,16 @@ extension EventsRoomExtension on Room {
name: filename,
);
}
final eventId = await pangeaSendTextEvent(
activity.markdown,
messageTag: ModelKey.messageTagActivityPlan,
);
if (file != null) {
await sendFileEvent(
file,
shrinkImageMaxDimension: 1600,
extraContent: {
ModelKey.messageTags: ModelKey.messageTagActivityPlan,
},
);
final content = <String, dynamic>{
'msgtype': file.msgType,
'body': file.name,
'filename': file.name,
'url': imageURL,
ModelKey.messageTags: ModelKey.messageTagActivityPlan,
};
await sendEvent(content);
}
if (canSendDefaultStates) {

View file

@ -0,0 +1,102 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people_view.dart';
import 'package:fluffychat/widgets/matrix.dart';
class FindYourPeople extends StatefulWidget {
const FindYourPeople({super.key});
@override
State<FindYourPeople> createState() => FindYourPeopleState();
}
class FindYourPeopleState extends State<FindYourPeople> {
final TextEditingController searchController = TextEditingController();
String? error;
bool loading = true;
Timer? _coolDown;
final List<PublicRoomsChunk> spaceItems = [];
@override
void initState() {
super.initState();
setSpaceItems();
}
@override
void dispose() {
searchController.dispose();
_coolDown?.cancel();
super.dispose();
}
void onSearchEnter(String text, {bool globalSearch = true}) {
if (text.isEmpty) {
setSpaceItems();
return;
}
_coolDown?.cancel();
_coolDown = Timer(const Duration(milliseconds: 500), setSpaceItems);
}
Future<void> setSpaceItems() async {
setState(() {
loading = true;
error = null;
spaceItems.clear();
});
try {
final resp = await Matrix.of(context).client.queryPublicRooms(
filter: PublicRoomQueryFilter(
roomTypes: ['m.space'],
genericSearchTerm: searchController.text,
),
limit: 100,
);
spaceItems.addAll(resp.chunk);
spaceItems.sort((a, b) {
int getPriority(item) {
final bool hasTopic = item.topic != null && item.topic!.isNotEmpty;
final bool hasAvatar = item.avatarUrl != null;
if (hasTopic && hasAvatar) return 0; // Highest priority
if (hasAvatar) return 1; // Second priority
if (hasTopic) return 2; // Third priority
return 3; // Lowest priority
}
return getPriority(a).compareTo(getPriority(b));
});
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'searchText': searchController.text,
},
);
error = e.toString();
} finally {
if (mounted) {
setState(() {
loading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return FindYourPeopleView(controller: this);
}
}

View file

@ -0,0 +1,3 @@
class FindYourPeopleConstants {
static const String sideBearFileName = "Bear_Find_your_people.png";
}

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people_constants.dart';
import 'package:fluffychat/widgets/navigation_rail.dart';
class FindYourPeopleSideView extends StatelessWidget {
const FindYourPeopleSideView({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
if (FluffyThemes.isColumnMode(context) ||
AppConfig.displayNavigationRail) ...[
SpacesNavigationRail(
activeSpaceId: null,
onGoToChats: () => context.go('/rooms'),
onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'),
),
Container(
color: Colors.transparent,
width: 1,
),
],
Expanded(
child: Center(
child: SizedBox(
width: 250.0,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${FindYourPeopleConstants.sideBearFileName}",
errorWidget: (context, url, error) => const SizedBox(),
placeholder: (context, url) => const Center(
child: CircularProgressIndicator.adaptive(),
),
),
),
),
),
],
);
}
}

View file

@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people.dart';
import 'package:fluffychat/pangea/find_your_people/public_space_tile.dart';
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
class FindYourPeopleView extends StatelessWidget {
final FindYourPeopleState controller;
const FindYourPeopleView({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isColumnMode = FluffyThemes.isColumnMode(context);
return Scaffold(
appBar: isColumnMode
? null
: AppBar(
leading: IconButton(
icon: Icon(
Icons.chevron_left,
color: theme.colorScheme.primary,
),
onPressed: () => Navigator.of(context).pop(),
),
title: Icon(
Icons.groups_outlined,
size: 24.0,
color: theme.colorScheme.primary,
),
centerTitle: false,
leadingWidth: 48.0,
actions: [
TextButton(
child: Row(
children: [
Icon(
Icons.join_full,
color: theme.colorScheme.primary,
size: 24.0,
),
const SizedBox(width: 8.0),
Text(
L10n.of(context).joinWithCode,
style: TextStyle(
color: theme.colorScheme.primary,
fontSize: 14.0,
),
),
],
),
onPressed: () =>
SpaceCodeUtil.joinWithSpaceCodeDialog(context),
),
],
),
floatingActionButton: isColumnMode
? null
: FloatingActionButton.extended(
onPressed: () => context.push('/rooms/newspace'),
icon: const Icon(Icons.add_box_outlined),
label: Text(
L10n.of(context).space,
overflow: TextOverflow.fade,
),
),
body: Padding(
padding: isColumnMode
? const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 20.0,
)
: const EdgeInsets.all(12.0),
child: Column(
spacing: 16.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isColumnMode)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 12.0,
),
child: Text(
L10n.of(context).findYourPeople,
style: const TextStyle(fontSize: 32.0),
),
),
Expanded(
child: Column(
spacing: isColumnMode ? 32.0 : 16.0,
children: [
Container(
height: 48.0,
padding: isColumnMode
? const EdgeInsets.symmetric(horizontal: 12)
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 10,
children: [
Expanded(
child: SizedBox(
height: 40.0,
child: TextField(
controller: controller.searchController,
onChanged: controller.onSearchEnter,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
filled: !isColumnMode,
fillColor: isColumnMode
? null
: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: isColumnMode
? const BorderSide()
: BorderSide.none,
borderRadius: BorderRadius.circular(100),
),
contentPadding:
const EdgeInsets.fromLTRB(0, 0, 20.0, 0),
hintText: L10n.of(context).findYourPeople,
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
fontSize: 16.0,
),
floatingLabelBehavior:
FloatingLabelBehavior.never,
prefixIcon: IconButton(
onPressed: () {},
icon: Icon(
Icons.search_outlined,
color: theme.colorScheme.onPrimaryContainer,
),
),
),
),
),
),
if (isColumnMode)
TextButton(
child: Row(
children: [
Icon(
Icons.join_full,
color: theme.colorScheme.onPrimaryContainer,
size: 24.0,
),
const SizedBox(width: 8.0),
Text(
L10n.of(context).joinWithCode,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontSize: 16.0,
),
),
],
),
onPressed: () =>
SpaceCodeUtil.joinWithSpaceCodeDialog(context),
),
if (isColumnMode)
TextButton(
child: Row(
children: [
Icon(
Icons.add_box_outlined,
color: theme.colorScheme.onPrimaryContainer,
size: 24.0,
),
const SizedBox(width: 8.0),
Text(
L10n.of(context).createYourSpace,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontSize: 16.0,
),
),
],
),
onPressed: () => context.push('/rooms/newspace'),
),
],
),
),
controller.error != null
? Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
L10n.of(context).oopsSomethingWentWrong,
),
IconButton(
onPressed: controller.setSpaceItems,
icon: const Icon(Icons.refresh),
),
],
)
: controller.loading
? const CircularProgressIndicator.adaptive()
: controller.spaceItems.isEmpty
? Text(
L10n.of(context).nothingFound,
)
: Expanded(
child: ListView.builder(
itemCount: controller.spaceItems.length,
itemBuilder: (context, index) {
final space =
controller.spaceItems[index];
return Padding(
padding: isColumnMode
? const EdgeInsets.only(
bottom: 32.0,
)
: const EdgeInsets.only(
bottom: 16.0,
),
child: PublicSpaceTile(space: space),
);
},
),
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicator_button.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/widgets/avatar.dart';
class PublicSpaceTile extends StatelessWidget {
final PublicRoomsChunk space;
const PublicSpaceTile({super.key, required this.space});
@override
Widget build(BuildContext context) {
final bool isColumnMode = FluffyThemes.isColumnMode(context);
return HoverButton(
onPressed: () => PublicRoomBottomSheet.show(
context: context,
chunk: space,
),
borderRadius: BorderRadius.circular(10.0),
hoverOpacity: 0.1,
child: Padding(
padding: isColumnMode
? const EdgeInsets.all(12.0)
: const EdgeInsets.all(0.0),
child: Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: isColumnMode ? 80.0 : 58.0,
child: Row(
children: [
Avatar(
mxContent: space.avatarUrl,
name: space.name,
size: isColumnMode ? 80.0 : 58.0,
borderRadius: BorderRadius.circular(
10,
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
space.name ?? '',
style: TextStyle(
fontSize: isColumnMode ? 20.0 : 14.0,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
spacing: 10,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.group,
size: isColumnMode ? 20.0 : 16.0,
),
Text(
L10n.of(context).countParticipants(
space.numJoinedMembers,
),
style: TextStyle(
fontSize: isColumnMode ? 16.0 : 12.0,
height: 1.2,
),
),
],
),
],
),
),
),
],
),
),
if (isColumnMode && space.topic != null && space.topic!.isNotEmpty)
Text(
space.topic!,
style: const TextStyle(
fontSize: 16.0,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}

View file

@ -296,13 +296,14 @@ class MessageTokenButtonContent extends StatelessWidget {
BorderRadius.circular(AppConfig.borderRadius - 4);
Color _color(BuildContext context) {
if (activity == null) {
return Theme.of(context).colorScheme.primary;
}
if (isActivityCompleteOrNullForToken) {
return AppConfig.gold;
}
return Theme.of(context).colorScheme.primary;
final bool isLight = Theme.of(context).brightness == Brightness.light;
final defaultColor = isLight
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primaryContainer;
return activity != null && isActivityCompleteOrNullForToken
? AppConfig.gold
: defaultColor;
}
@override
@ -377,6 +378,8 @@ class MessageTokenButtonContent extends StatelessWidget {
(selectedChoice != null ? 0.4 : 0.0) +
(accepted.isNotEmpty ? 0.3 : 0.0);
final theme = Theme.of(context);
return InkWell(
onTap: selectedChoice != null
? () => onMatch?.call(selectedChoice!)
@ -384,10 +387,11 @@ class MessageTokenButtonContent extends StatelessWidget {
borderRadius: _borderRadius,
child: CustomPaint(
painter: DottedBorderPainter(
color: Theme.of(context)
.colorScheme
.primary
.withAlpha((colorAlpha * 255).toInt()),
color: theme.brightness == Brightness.light
? theme.colorScheme.primary
.withAlpha((colorAlpha * 255).toInt())
: theme.colorScheme.primaryContainer
.withAlpha((colorAlpha * 255).toInt()),
borderRadius: _borderRadius,
),
child: Container(
@ -396,9 +400,7 @@ class MessageTokenButtonContent extends StatelessWidget {
width: max(width, 24.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
color: theme.colorScheme.primary
.withAlpha((max(0, colorAlpha - 0.7) * 255).toInt()),
borderRadius: _borderRadius,
),

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

@ -142,7 +142,7 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
context: context,
future: () async => client.knockRoom(
roomAlias ?? chunk!.roomId,
serverName: via,
via: via,
),
onSuccess: () => L10n.of(context).knockSpaceSuccess,
delay: false,

View file

@ -0,0 +1,174 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
class LeaderboardParticipantList extends StatefulWidget {
final Room space;
const LeaderboardParticipantList({
required this.space,
super.key,
});
static const double height = 116;
@override
State<LeaderboardParticipantList> createState() =>
LeaderboardParticipantListState();
}
class LeaderboardParticipantListState
extends State<LeaderboardParticipantList> {
final _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
final theme = Theme.of(context);
return StreamBuilder(
stream: client.onSync.stream.rateLimit(const Duration(seconds: 3)),
builder: (context, snapshot) {
return LoadParticipantsUtil(
space: widget.space,
builder: (participantsLoader) {
final participants = participantsLoader.filteredParticipants("");
return AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: Curves.easeInOut,
child: SizedBox(
height: 130.0,
child: Scrollbar(
controller: _scrollController,
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(
8.0,
8.0,
8.0,
16.0,
),
scrollDirection: Axis.horizontal,
itemCount: participants.length,
itemBuilder: (context, i) {
final user = participants[i];
final publicProfile = participantsLoader.getPublicProfile(
user.id,
);
LinearGradient? gradient = i.leaderboardGradient;
if (user.id == BotName.byEnvironment ||
publicProfile == null ||
publicProfile.level == null) {
gradient = null;
}
return PresenceBuilder(
userId: user.id,
builder: (context, presence) {
Color? dotColor;
if (presence != null) {
dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
}
return PresenceAvatar(
presence: presence ??
CachedPresence(
PresenceType.unavailable,
null,
null,
null,
user.id,
),
height: StatusMessageList.height,
onTap: (profile) => UserDialog.show(
context: context,
profile: profile,
),
gradient: gradient,
showPresence: false,
floatingIndicator: Positioned(
bottom: 0,
right: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(32),
),
alignment: Alignment.center,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
width: 1,
color: theme.colorScheme.surface,
),
),
),
),
),
);
},
);
},
),
),
),
);
},
);
},
);
}
}
extension LeaderboardGradient on int {
LinearGradient? get leaderboardGradient {
final Color? color = this == 0
? AppConfig.gold
: this == 1
? Colors.grey[400]!
: this == 2
? Colors.brown[400]!
: null;
if (color == null) return null;
return LinearGradient(
colors: [
color,
Colors.white,
color,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
}

View file

@ -282,7 +282,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

@ -91,8 +91,16 @@ class WordAudioButtonState extends State<WordAudioButton> {
context: context,
targetID: 'word-audio-button-${widget.uniqueID}',
langCode: widget.langCode,
onStart: () => setState(() => _isPlaying = true),
onStop: () => setState(() => _isPlaying = false),
onStart: () {
if (mounted) {
setState(() => _isPlaying = true);
}
},
onStop: () {
if (mounted) {
setState(() => _isPlaying = false);
}
},
);
}
},

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

@ -1,4 +0,0 @@
class AgeLimits {
static const int toAccessFeatures = 18;
static const int toUseTheApp = 13;
}

View file

@ -1,27 +0,0 @@
import 'user_model.dart';
class UserProfileSearchResponse {
int count;
String? next;
String? previous;
List<PangeaProfile> results;
UserProfileSearchResponse({
required this.count,
required this.next,
required this.previous,
required this.results,
});
factory UserProfileSearchResponse.fromJson(Map<String, dynamic> json) {
return UserProfileSearchResponse(
count: json["count"],
next: json["next"],
previous: json["previous"],
results: json["results"]
.map((p) => PangeaProfile.fromJson(p))
.toList()
.cast<PangeaProfile>(),
);
}
}

View file

@ -1,179 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:country_picker/country_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/user/models/user_model.dart';
import '../../../widgets/matrix.dart';
import '../../common/controllers/pangea_controller.dart';
import '../models/user_profile_search_model.dart';
import '../repo/user_repo.dart';
import 'find_partner_view.dart';
class FindPartner extends StatefulWidget {
const FindPartner({super.key});
@override
State<FindPartner> createState() => FindPartnerController();
}
class FindPartnerController extends State<FindPartner> {
PangeaController pangeaController = MatrixState.pangeaController;
bool initialLoad = true;
bool loading = false;
String currentSearchTerm = "";
late LanguageModel targetLanguageSearch;
late LanguageModel sourceLanguageSearch;
String? countrySearch;
String? flagEmoji;
//PTODO - implement pagination
String? nextUrl = "";
int nextPage = 1;
Timer? coolDown;
final List<PangeaProfile> _userProfilesCache = [];
final scrollController = ScrollController();
String? error;
@override
void initState() {
targetLanguageSearch = pangeaController.languageController.userL1 ??
pangeaController.pLanguageStore.targetOptions[1];
sourceLanguageSearch = pangeaController.languageController.userL2 ??
pangeaController.pLanguageStore.targetOptions[0];
scrollController.addListener(() {
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
searchUserProfiles();
}
});
searchUserProfiles().then((_) => setState(() => initialLoad = false));
super.initState();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (error != null && error!.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).oopsSomethingWentWrong),
Text(L10n.of(context).errorPleaseRefresh),
],
),
);
}
return FindPartnerView(this);
}
List<PangeaProfile> get userProfiles => _userProfilesCache.where((p) {
return (p.targetLanguage != null &&
targetLanguageSearch.langCode == p.targetLanguage) &&
(p.sourceLanguage != null &&
sourceLanguageSearch.langCode == p.sourceLanguage) &&
(countrySearch == null ||
(p.country != null && countrySearch == p.country));
}).toList();
void searchUserProfilesWithCoolDown(String text) {
coolDown?.cancel();
coolDown = Timer(
const Duration(milliseconds: 0),
() => searchUserProfiles(),
);
}
Future<void> searchUserProfiles() async {
coolDown?.cancel();
if (loading || nextUrl == null) return;
setState(() => loading = true);
UserProfileSearchResponse response;
try {
final String accessToken = pangeaController.userController.accessToken;
response = await PUserRepo.searchUserProfiles(
accessToken: accessToken,
targetLanguage: targetLanguageSearch.langCode,
sourceLanguage: sourceLanguageSearch.langCode,
country: countrySearch,
limit: 15,
pageNumber: nextPage.toString(),
);
} catch (err, s) {
error = err.toString();
setState(() => loading = false);
ErrorHandler.logError(
e: err,
s: s,
data: {
"accessToken": pangeaController.userController.accessToken,
"targetLanguage": targetLanguageSearch.langCode,
"sourceLanguage": sourceLanguageSearch.langCode,
"country": countrySearch,
"pageNumber": nextPage.toString(),
},
);
return;
}
nextUrl = response.next;
nextPage++;
final String? currentUserId = pangeaController.matrixState.client.userID;
_userProfilesCache.addAll(
response.results.where(
(p) =>
!_userProfilesCache.any(
(element) => p.pangeaUserId == element.pangeaUserId,
) &&
p.pangeaUserId != currentUserId,
),
);
setState(() => loading = false);
}
Future<void> filterUserProfiles({
LanguageModel? targetLanguage,
LanguageModel? sourceLanguage,
Country? country,
}) async {
if (country != null) {
if (country.name != "World Wide") {
countrySearch = country.displayNameNoCountryCode;
flagEmoji = country.flagEmoji;
} else {
countrySearch = null;
flagEmoji = null;
}
}
if (targetLanguage != null) {
targetLanguageSearch = targetLanguage;
}
if (sourceLanguage != null) {
sourceLanguageSearch = sourceLanguage;
}
nextPage = 1;
nextUrl = "";
await searchUserProfiles();
setState(() {});
}
}

View file

@ -1,315 +0,0 @@
import 'package:flutter/material.dart';
import 'package:country_picker/country_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/learning_settings/utils/country_display.dart';
import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dropdown.dart';
import 'package:fluffychat/pangea/user/models/user_model.dart';
import 'package:fluffychat/pangea/user/widgets/list_placeholder.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../../widgets/profile_bottom_sheet.dart';
import 'find_partner.dart';
class FindPartnerView extends StatelessWidget {
final FindPartnerController controller;
const FindPartnerView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () => context.pop(),
),
centerTitle: true,
title: const PageTitleText(),
),
body: Center(
child: Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2,
minWidth: FluffyThemes.columnWidth * 2,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
LanguageSelectionRow(
controller: controller,
isSource: true,
),
LanguageSelectionRow(
controller: controller,
isSource: false,
),
Padding(
padding: const EdgeInsets.all(18),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
L10n.of(context).iWantALanguagePartnerFrom,
style: const TextStyle(fontSize: 16),
),
Row(
children: [
Text(
controller.countrySearch ??
L10n.of(context).worldWide,
),
Padding(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
child: controller.flagEmoji != null
? RichText(
text: TextSpan(
text: controller.flagEmoji,
style: const TextStyle(fontSize: 30),
),
)
: const PangeaLogoSvg(width: 30),
),
IconButton(
icon: const Icon(Icons.expand_more),
onPressed: () => showCountryPicker(
showWorldWide: true,
context: context,
showPhoneCode: false,
onSelect: (Country country) {
controller.filterUserProfiles(
country: country,
);
},
),
),
],
),
],
),
),
controller.initialLoad
? const ExpandedContainer(body: ListPlaceholder())
: controller.userProfiles.isNotEmpty
? ExpandedContainer(
body: ListView.builder(
controller: controller.scrollController,
itemCount: controller.userProfiles.length + 1,
itemBuilder: (context, i) => i !=
controller.userProfiles.length
? UserProfileEntry(
pangeaProfile: controller.userProfiles[i],
controller: controller,
)
: controller.loading
? const Center(
child: CircularProgressIndicator
.adaptive(),
)
: const SizedBox.shrink(),
),
)
: ExpandedContainer(
body: Center(
child: Text(L10n.of(context).noResults),
),
),
],
),
),
),
);
}
}
class ExpandedContainer extends StatelessWidget {
const ExpandedContainer({
super.key,
required this.body,
});
final Widget body;
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
margin: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: body,
),
);
}
}
class ProfileSearchTextField extends StatelessWidget {
const ProfileSearchTextField({
super.key,
required this.controller,
});
final FindPartnerController controller;
@override
Widget build(BuildContext context) {
return TextField(
autofocus: true,
decoration: InputDecoration(
hintText: L10n.of(context).searchBy,
suffixIconConstraints: const BoxConstraints(
maxWidth: 48,
maxHeight: 48,
minWidth: 48,
),
suffixIcon: controller.initialLoad
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.search_outlined),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
onChanged: controller.searchUserProfilesWithCoolDown,
);
}
}
class PageTitleText extends StatelessWidget {
const PageTitleText({
super.key,
});
@override
Widget build(BuildContext context) {
return FittedBox(
child: Text(
L10n.of(context).iWantAConversationPartner,
style: TextStyle(
color: Theme.of(context).textTheme.bodyLarge!.color,
fontSize: 18,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
);
}
}
class LanguageSelectionRow extends StatelessWidget {
const LanguageSelectionRow({
super.key,
required this.controller,
required this.isSource,
});
final FindPartnerController controller;
final bool isSource;
@override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: ListTile(
title: isSource
? Text(
L10n.of(context).iWantALanguagePartnerWhoSpeaks,
style: const TextStyle(fontSize: 16),
)
: Text(
L10n.of(context).iWantALanguagePartnerWhoIsLearning,
style: const TextStyle(fontSize: 16),
),
),
),
Flexible(
child: PLanguageDropdown(
languages: isSource
? controller.pangeaController.pLanguageStore.baseOptions
: controller.pangeaController.pLanguageStore.targetOptions,
onChange: (language) {
controller.filterUserProfiles(
sourceLanguage: isSource ? language : null,
targetLanguage: isSource ? null : language,
);
},
isL2List: !isSource,
initialLanguage: isSource
? controller.sourceLanguageSearch
: controller.targetLanguageSearch,
decorationText: isSource
? L10n.of(context).myBaseLanguage
: L10n.of(context).iWantToLearn,
),
),
],
);
}
}
class UserProfileEntry extends StatelessWidget {
final PangeaProfile pangeaProfile;
final FindPartnerController controller;
const UserProfileEntry({
super.key,
required this.pangeaProfile,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FutureBuilder<matrix.Profile>(
future: Matrix.of(context)
.client
.getProfileFromUserId(pangeaProfile.pangeaUserId),
builder: ((context, snapshot) {
final matrixProfile = snapshot.data;
return ListTile(
leading: Avatar(
name: matrixProfile == null || matrixProfile.avatarUrl == null
? pangeaProfile.pangeaUserId
: null,
mxContent: matrixProfile?.avatarUrl,
),
title: Row(
children: [
Flexible(
child: Text(
//PTODO - get matrix u and show displayName
matrixProfile?.displayName ??
pangeaProfile.pangeaUserId.replaceAll(
":${AppConfig.defaultHomeserver.replaceAll("matrix.", "")}",
"",
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 20),
RichText(
text: TextSpan(
text: CountryDisplayUtil.flagEmoji(pangeaProfile.country),
style: const TextStyle(fontSize: 15),
),
),
],
),
onTap: () => showModalBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId: pangeaProfile.pangeaUserId,
outerContext: context,
),
),
);
}),
),
],
);
}
}

View file

@ -1,48 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import '../../common/network/requests.dart';
import '../../common/network/urls.dart';
import '../models/user_profile_search_model.dart';
class PUserRepo {
static Future<UserProfileSearchResponse> searchUserProfiles({
// List<String>? interests,
String? targetLanguage,
String? sourceLanguage,
String? country,
// String? speaks,
String? pageNumber,
required String accessToken,
required int limit,
}) async {
final Requests req = Requests(
accessToken: accessToken,
choreoApiKey: Environment.choreoApiKey,
);
final Map<String, dynamic> body = {};
// if (interests != null) body[ModelKey.userInterests] = interests.toString();
if (targetLanguage != null) {
body[ModelKey.userTargetLanguage] = targetLanguage;
}
if (sourceLanguage != null) {
body[ModelKey.userSourceLanguage] = sourceLanguage;
}
if (country != null) body[ModelKey.userCountry] = country;
final String searchUrl =
"${PApiUrls.searchUserProfiles}?limit=$limit${pageNumber != null ? '&page=$pageNumber' : ''}";
final Response res = await req.post(
url: searchUrl,
body: body,
);
//PTODO - implement paginiation - make another call with next url
return UserProfileSearchResponse.fromJson(jsonDecode(res.body));
}
}

View file

@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
class ListPlaceholder extends StatelessWidget {
static const dummyChatCount = 5;
const ListPlaceholder({super.key});
@override
Widget build(BuildContext context) {
final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
return ListView.builder(
itemCount: dummyChatCount,
itemBuilder: (context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: Material(
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
),
);
}
}

View file

@ -1,113 +0,0 @@
// // presents choices from vocab_bank_repo
// // displays them as emoji choices
// // once selection, these words are inserted into the input bar
// import 'dart:async';
// import 'package:fluffychat/config/themes.dart';
// import 'package:fluffychat/pages/chat/chat.dart';
// import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
// import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
// import 'package:fluffychat/pangea/emojis/emoji_stack.dart';
// import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
// import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_emoji_choice_item.dart';
// import 'package:fluffychat/pangea/word_bank/vocab_bank_repo.dart';
// import 'package:fluffychat/pangea/word_bank/vocab_request.dart';
// import 'package:fluffychat/pangea/word_bank/vocab_response.dart';
// import 'package:fluffychat/widgets/matrix.dart';
// import 'package:flutter/material.dart';
// class WritingAssistanceInputRow extends StatefulWidget {
// final ChatController controller;
// const WritingAssistanceInputRow(
// this.controller, {
// super.key,
// });
// @override
// WritingAssistanceInputRowState createState() =>
// WritingAssistanceInputRowState();
// }
// class WritingAssistanceInputRowState extends State<WritingAssistanceInputRow> {
// List<ConstructIdentifier> suggestions = [];
// StreamSubscription? _choreoSub;
// Choreographer get choreographer => widget.controller.choreographer;
// @override
// void initState() {
// // Rebuild the widget each time there's an update from choreo
// _choreoSub = choreographer.stateListener.stream.listen((_) {
// setSuggestions();
// });
// setSuggestions();
// super.initState();
// }
// @override
// void dispose() {
// _choreoSub?.cancel();
// super.dispose();
// }
// Future<void> setSuggestions() async {
// final String currentText = choreographer.currentText;
// final VocabRequest request = VocabRequest(
// langCode: MatrixState
// .pangeaController.languageController.userL2?.langCodeShort ??
// LanguageKeys.defaultLanguage,
// level: MatrixState
// .pangeaController.userController.profile.userSettings.cefrLevel,
// prefix: currentText,
// );
// final VocabResponse response = await VocabRepo.get(request);
// setState(() {
// suggestions = response.vocab;
// });
// }
// @override
// Widget build(BuildContext context) {
// return AnimatedContainer(
// duration: FluffyThemes.animationDuration,
// curve: FluffyThemes.animationCurve,
// child: SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Row(
// children: suggestions
// .map(
// (suggestion) => MessageEmojiChoiceItem(
// topContent: EmojiStack(
// emoji: suggestion.userSetEmoji,
// // suggestion.userSetEmoji ??
// // MatrixState
// // .pangeaController.getAnalytics.constructListModel
// // .getConstructUses(suggestion)
// // ?.xpEmoji ??
// // AnalyticsConstants.emojiForSeed,
// style: const TextStyle(fontSize: 24),
// ),
// content: suggestion.lemma,
// onTap: () {
// choreographer.onPredictorSelect(suggestion.lemma);
// // setState(() {
// // suggestions = [];
// // });
// },
// isSelected: false,
// textSize: 16,
// greenHighlight: false,
// ),
// )
// .toList(),
// ),
// ),
// );
// }
// }

View file

@ -132,9 +132,6 @@ abstract class ClientManager {
},
logLevel: kReleaseMode ? Level.warning : Level.verbose,
databaseBuilder: flutterMatrixSdkDatabaseBuilder,
// #Pangea
// legacyDatabaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder,
// Pangea#
supportedLoginTypes: {
AuthenticationTypes.password,
AuthenticationTypes.sso,

View file

@ -1,149 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart' hide Key;
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:universal_html/html.dart' as html;
// ignore: deprecated_member_use
class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
FlutterHiveCollectionsDatabase(
super.name,
String super.path, {
super.key,
});
static const String _cipherStorageKey = 'hive_encryption_key';
static Future<FlutterHiveCollectionsDatabase> databaseBuilder(
Client client,
) async {
Logs().d('Open Hive...');
HiveAesCipher? hiverCipher;
try {
// Workaround for secure storage is calling Platform.operatingSystem on web
if (kIsWeb) {
// ignore: unawaited_futures
html.window.navigator.storage?.persist();
throw MissingPluginException();
}
const secureStorage = FlutterSecureStorage();
final containsEncryptionKey =
await secureStorage.read(key: _cipherStorageKey) != null;
if (!containsEncryptionKey) {
// do not try to create a buggy secure storage for new Linux users
if (Platform.isLinux) throw MissingPluginException();
final key = Hive.generateSecureKey();
await secureStorage.write(
key: _cipherStorageKey,
value: base64UrlEncode(key),
);
}
// workaround for if we just wrote to the key and it still doesn't exist
final rawEncryptionKey = await secureStorage.read(key: _cipherStorageKey);
if (rawEncryptionKey == null) throw MissingPluginException();
hiverCipher = HiveAesCipher(base64Url.decode(rawEncryptionKey));
} on MissingPluginException catch (_) {
const FlutterSecureStorage()
.delete(key: _cipherStorageKey)
.catchError((_) {});
Logs().i('Hive encryption is not supported on this platform');
} catch (e, s) {
const FlutterSecureStorage()
.delete(key: _cipherStorageKey)
.catchError((_) {});
Logs().w('Unable to init Hive encryption', e, s);
}
final db = FlutterHiveCollectionsDatabase(
'hive_collections_${client.clientName.replaceAll(' ', '_').toLowerCase()}',
await findDatabasePath(client),
key: hiverCipher,
);
try {
await db.open();
} catch (e, s) {
Logs().w('Unable to open Hive. Delete database and storage key...', e, s);
const FlutterSecureStorage().delete(key: _cipherStorageKey);
await db.clear().catchError((_) {});
await Hive.deleteFromDisk();
rethrow;
}
Logs().d('Hive is ready');
return db;
}
static Future<String> findDatabasePath(Client client) async {
var path = client.clientName;
if (!kIsWeb) {
Directory directory;
try {
if (Platform.isLinux) {
directory = await getApplicationSupportDirectory();
} else {
directory = await getApplicationDocumentsDirectory();
}
} catch (_) {
try {
directory = await getLibraryDirectory();
} catch (_) {
directory = Directory.current;
}
}
// do not destroy your stable FluffyChat in debug mode
directory = Directory(
directory.uri.resolve(kDebugMode ? 'hive_debug' : 'hive').toFilePath(),
);
directory.create(recursive: true);
path = directory.path;
}
return path;
}
@override
int get maxFileSize => supportsFileStoring ? 100 * 1000 * 1000 : 0;
@override
bool get supportsFileStoring => !kIsWeb;
Future<String> _getFileStoreDirectory() async {
try {
try {
return (await getTemporaryDirectory()).path;
} catch (_) {
return (await getApplicationDocumentsDirectory()).path;
}
} catch (_) {
return (await getDownloadsDirectory())!.path;
}
}
@override
Future<Uint8List?> getFile(Uri mxcUri) async {
if (!supportsFileStoring) return null;
final tempDirectory = await _getFileStoreDirectory();
final file =
File('$tempDirectory/${Uri.encodeComponent(mxcUri.toString())}');
if (await file.exists() == false) return null;
final bytes = await file.readAsBytes();
return bytes;
}
@override
Future storeFile(Uri mxcUri, Uint8List bytes, int time) async {
if (!supportsFileStoring) return null;
final tempDirectory = await _getFileStoreDirectory();
final file =
File('$tempDirectory/${Uri.encodeComponent(mxcUri.toString())}');
if (await file.exists()) return;
await file.writeAsBytes(bytes);
return;
}
}

View file

@ -10,7 +10,6 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:universal_html/html.dart' as html;
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'cipher.dart';
@ -74,7 +73,7 @@ Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
Logs().e('Unable to send error notification', e, s);
}
return FlutterHiveCollectionsDatabase.databaseBuilder(client);
rethrow;
}
}

View file

@ -1,4 +1,5 @@
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:intl/intl.dart';
import 'package:matrix/matrix.dart';
/// This is a temporary helper class until there is a proper solution to this with the new system
@ -353,4 +354,21 @@ class MatrixLocals extends MatrixLocalizations {
@override
String get cancelledSend => l10n.sendCanceled;
@override
String voiceMessage(String senderName, Duration? duration) {
final dateTime = duration == null
? null
: DateTime.fromMillisecondsSinceEpoch(
duration.inSeconds * 1000,
);
final formattedDuration = dateTime == null
? ''
: DateFormat(
DateFormat.MINUTE_SECOND,
l10n.localeName,
).format(dateTime);
return l10n.sentVoiceMessage(senderName, formattedDuration);
}
}

View file

@ -87,7 +87,7 @@ Future<void> _tryPushHelper(
.first;
final event = await client.getEventByPushNotification(
notification,
storeInDatabase: isBackgroundMessage,
storeInDatabase: false,
);
if (event == null) {

View file

@ -191,7 +191,6 @@ class UrlLauncher {
await PublicRoomBottomSheet.show(
context: context,
roomAlias: identityParts.primaryIdentifier,
// Pangea#
);
// Pangea#
}

View file

@ -71,50 +71,48 @@ class Avatar extends StatelessWidget {
borderRadius: borderRadius,
side: border ?? BorderSide.none,
),
clipBehavior: Clip.hardEdge,
child:
// #Pangea
(userId ?? presenceUserId) == BotName.byEnvironment
? BotFace(
width: size,
expression: BotExpression.idle,
useRive: useRive,
)
:
clipBehavior: Clip.antiAlias,
// #Pangea
// child: noPic
child: (userId ?? presenceUserId) == BotName.byEnvironment
? BotFace(
width: size,
expression: BotExpression.idle,
useRive: useRive,
)
: noPic
// Pangea#
noPic
? Container(
decoration:
BoxDecoration(color: name?.lightColorAvatar),
alignment: Alignment.center,
child: Text(
fallbackLetters,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'RobotoMono',
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: (size / 2.5).roundToDouble(),
),
),
)
: MxcImage(
client: client,
key: ValueKey(mxContent.toString()),
cacheKey: '${mxContent}_$size',
uri: mxContent,
fit: BoxFit.cover,
width: size,
height: size,
placeholder: (_) => Center(
child: Icon(
Icons.person_2,
color: theme.colorScheme.tertiary,
size: size / 1.5,
),
),
? Container(
decoration:
BoxDecoration(color: name?.lightColorAvatar),
alignment: Alignment.center,
child: Text(
fallbackLetters,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'RobotoMono',
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: (size / 2.5).roundToDouble(),
),
),
)
: MxcImage(
client: client,
key: ValueKey(mxContent.toString()),
cacheKey: '${mxContent}_$size',
uri: mxContent,
fit: BoxFit.cover,
width: size,
height: size,
placeholder: (_) => Center(
child: Icon(
Icons.person_2,
color: theme.colorScheme.tertiary,
size: size / 1.5,
),
),
),
),
),
// #Pangea

View file

@ -5,11 +5,17 @@ import 'package:fluffychat/config/themes.dart';
class TwoColumnLayout extends StatelessWidget {
final Widget mainView;
final Widget sideView;
// #Pangea
final Color? dividerColor;
// Pangea#
const TwoColumnLayout({
super.key,
required this.mainView,
required this.sideView,
// #Pangea
this.dividerColor,
// Pangea#
});
@override
Widget build(BuildContext context) {
@ -27,7 +33,10 @@ class TwoColumnLayout extends StatelessWidget {
),
Container(
width: 1.0,
color: theme.dividerColor,
// #Pangea
// color: theme.dividerColor,
color: dividerColor ?? theme.dividerColor,
// Pangea#
),
Expanded(
child: ClipRRect(

View file

@ -7,7 +7,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/navi_rail_item.dart';
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -17,11 +16,17 @@ class SpacesNavigationRail extends StatelessWidget {
final String? activeSpaceId;
final void Function() onGoToChats;
final void Function(String) onGoToSpaceId;
// #Pangea
final void Function()? clearActiveSpace;
// Pangea#
const SpacesNavigationRail({
required this.activeSpaceId,
required this.onGoToChats,
required this.onGoToSpaceId,
// #Pangea
this.clearActiveSpace,
// Pangea#
super.key,
});
@ -35,13 +40,14 @@ class SpacesNavigationRail extends StatelessWidget {
.path
.startsWith('/rooms/settings');
// #Pangea
final isHomepage = GoRouter.of(context)
.routeInformationProvider
.value
.uri
.path
.contains('homepage');
final path = GoRouter.of(context).routeInformationProvider.value.uri.path;
final isHomepage = path.contains('homepage');
final isCommunities = path.contains('communities');
final isColumnMode = FluffyThemes.isColumnMode(context);
final width = isColumnMode
? FluffyThemes.navRailWidth
: FluffyThemes.navRailWidth - 8.0;
// return StreamBuilder(
return Material(
child: SafeArea(
@ -67,9 +73,7 @@ class SpacesNavigationRail extends StatelessWidget {
return SizedBox(
// #Pangea
// width: FluffyThemes.navRailWidth,
width: isColumnMode
? FluffyThemes.navRailWidth
: FluffyThemes.navRailWidth * 0.75,
width: width,
// Pangea#
child: Column(
children: [
@ -78,18 +82,17 @@ class SpacesNavigationRail extends StatelessWidget {
scrollDirection: Axis.vertical,
// #Pangea
// itemCount: rootSpaces.length + 2,
itemCount: rootSpaces.length + 4,
itemCount: rootSpaces.length + 3,
// Pangea#
itemBuilder: (context, i) {
// #Pangea
if (i == 0) {
return NaviRailItem(
isSelected: isColumnMode
? activeSpaceId == null && !isSettings
: isHomepage,
onTap: () => isColumnMode
? onGoToChats()
: context.go("/rooms/homepage"),
isSelected: isHomepage,
onTap: () {
clearActiveSpace?.call();
context.go("/rooms/homepage");
},
backgroundColor: Colors.transparent,
icon: FutureBuilder<Profile>(
future: client.fetchOwnProfile(),
@ -103,7 +106,8 @@ class SpacesNavigationRail extends StatelessWidget {
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
client.userID!.localpart,
size: 45,
size:
width - (isColumnMode ? 32.0 : 24.0),
),
),
],
@ -115,52 +119,50 @@ class SpacesNavigationRail extends StatelessWidget {
i--;
// Pangea#
if (i == 0) {
return isColumnMode
? const SizedBox()
: NaviRailItem(
// #Pangea
// isSelected: activeSpaceId == null && !isSettings,
isSelected: activeSpaceId == null &&
!isSettings &&
!isHomepage,
// Pangea#
onTap: onGoToChats,
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),
),
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) {
// #Pangea
return NaviRailItem(
isSelected: false,
onTap: () =>
SpaceCodeUtil.joinWithSpaceCodeDialog(context),
icon: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.join_right_outlined),
),
toolTip: L10n.of(context).joinByCode,
);
}
if (i == rootSpaces.length + 1) {
// Pangea#
return NaviRailItem(
isSelected: false,
onTap: () => context.go('/rooms/newspace'),
icon: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.add),
),
toolTip: L10n.of(context).createNewSpace,
// #Pangea
// isSelected: false,
// onTap: () => context.go('/rooms/newspace'),
// icon: const Padding(
// padding: EdgeInsets.all(8.0),
// child: Icon(Icons.add),
// ),
// toolTip: L10n.of(context).createNewSpace,
isSelected: isCommunities,
onTap: () {
clearActiveSpace?.call();
context.go('/rooms/communities');
},
icon: const Icon(Icons.groups),
toolTip: L10n.of(context).findYourPeople,
// Pangea#
);
}
final space = rootSpaces[i];
@ -186,6 +188,9 @@ class SpacesNavigationRail extends StatelessWidget {
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 2,
),
// #Pangea
size: width - (isColumnMode ? 32.0 : 24.0),
// Pangea#
),
);
},
@ -194,14 +199,18 @@ class SpacesNavigationRail extends StatelessWidget {
NaviRailItem(
isSelected: isSettings,
onTap: () => context.go('/rooms/settings'),
icon: const Padding(
padding: EdgeInsets.all(10.0),
child: Icon(Icons.settings_outlined),
),
selectedIcon: const Padding(
padding: EdgeInsets.all(10.0),
child: Icon(Icons.settings),
),
// #Pangea
// icon: const Padding(
// padding: EdgeInsets.all(10.0),
// child: Icon(Icons.settings_outlined),
// ),
// selectedIcon: const Padding(
// padding: EdgeInsets.all(10.0),
// child: Icon(Icons.settings),
// ),
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
// Pangea#
toolTip: L10n.of(context).settings,
),
],

View file

@ -72,6 +72,13 @@ Future<int?> showPermissionChooser(
onPressed: () => Navigator.of(context).pop<int>(0),
child: Text(L10n.of(context).normalUser),
),
// #Pangea
AdaptiveDialogAction(
bigButtons: true,
onPressed: () => Navigator.of(context).pop(),
child: Text(L10n.of(context).close),
),
// Pangea#
],
),
);

View file

@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -71,6 +72,9 @@ class _ShareScaffoldDialogState extends State<ShareScaffoldDialog> {
(room) =>
room.canSendDefaultMessages &&
!room.isSpace &&
// #Pangea
!room.isAnalyticsRoom &&
// Pangea#
room.membership == Membership.join,
)
.toList();

View file

@ -0,0 +1,32 @@
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")

View file

@ -0,0 +1,5 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ dependencies:
async: ^2.11.0
badges: ^3.1.2
blurhash_dart: ^1.2.1
chewie: ^1.8.1
chewie: ^1.11.0
collection: ^1.18.0
cross_file: ^0.3.4+2
cupertino_icons: any
@ -56,7 +56,7 @@ dependencies:
flutter_web_auth_2: ^3.1.1 # Version 4 blocked by https://github.com/MixinNetwork/flutter-plugins/issues/379
flutter_webrtc: ^0.12.9
geolocator: ^13.0.1
go_router: ^14.8.1
go_router: ^15.1.2
handy_window: ^0.4.0
hive: ^2.2.3
hive_flutter: ^1.1.0
@ -72,8 +72,8 @@ dependencies:
matrix:
git:
url: https://github.com/pangeachat/matrix-dart-sdk.git # repo
ref: disable-space-hierarchy-cache # branch
# matrix: ^0.39.1
ref: main
# matrix: ^0.40.0
# Pangea#
mime: ^1.0.6
native_imaging: ^0.2.0

View file

@ -3,7 +3,7 @@
import 'package:matrix/encryption/utils/key_verification.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart';
Future<Client> prepareTestClient({
bool loggedIn = false,
@ -22,7 +22,7 @@ Future<Client> prepareTestClient({
importantStateEvents: <String>{
'im.ponies.room_emotes', // we want emotes to work properly
},
databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder,
databaseBuilder: flutterMatrixSdkDatabaseBuilder,
supportedLoginTypes: {
AuthenticationTypes.password,
AuthenticationTypes.sso,