Merge branch 'main' into 2840-show-all-lemma-emojis
This commit is contained in:
commit
15d9df3966
96 changed files with 2551 additions and 1958 deletions
30
.github/workflows/issue_closed.yaml
vendored
30
.github/workflows/issue_closed.yaml
vendored
|
|
@ -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.
|
||||
|
|
|
|||
20
.github/workflows/issue_opened_project.yaml
vendored
Normal file
20
.github/workflows/issue_opened_project.yaml
vendored
Normal 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 } } }'
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6083,4 +6083,4 @@
|
|||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4382,4 +4382,4 @@
|
|||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>"; };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
autoPlay: true,
|
||||
autoInitialize: true,
|
||||
);
|
||||
|
||||
// #Pangea
|
||||
_stopVideoSubscription?.cancel();
|
||||
_stopVideoSubscription =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() {});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ////////
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
102
lib/pangea/find_your_people/find_your_people.dart
Normal file
102
lib/pangea/find_your_people/find_your_people.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
class FindYourPeopleConstants {
|
||||
static const String sideBearFileName = "Bear_Find_your_people.png";
|
||||
}
|
||||
48
lib/pangea/find_your_people/find_your_people_side_view.dart
Normal file
48
lib/pangea/find_your_people/find_your_people_side_view.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
243
lib/pangea/find_your_people/find_your_people_view.dart
Normal file
243
lib/pangea/find_your_people/find_your_people_view.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
105
lib/pangea/find_your_people/public_space_tile.dart
Normal file
105
lib/pangea/find_your_people/public_space_tile.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
109
lib/pangea/onboarding/onboarding.dart
Normal file
109
lib/pangea/onboarding/onboarding.dart
Normal 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);
|
||||
}
|
||||
71
lib/pangea/onboarding/onboarding_complete.dart
Normal file
71
lib/pangea/onboarding/onboarding_complete.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
3
lib/pangea/onboarding/onboarding_constants.dart
Normal file
3
lib/pangea/onboarding/onboarding_constants.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class OnboardingConstants {
|
||||
static String onboardingImageFileName = "Getting+Started.png";
|
||||
}
|
||||
104
lib/pangea/onboarding/onboarding_step.dart
Normal file
104
lib/pangea/onboarding/onboarding_step.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/pangea/onboarding/onboarding_steps_enum.dart
Normal file
55
lib/pangea/onboarding/onboarding_steps_enum.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
130
lib/pangea/onboarding/onboarding_view.dart
Normal file
130
lib/pangea/onboarding/onboarding_view.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
174
lib/pangea/spaces/widgets/leaderboard_participant_list.dart
Normal file
174
lib/pangea/spaces/widgets/leaderboard_participant_list.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
class AgeLimits {
|
||||
static const int toAccessFeatures = 18;
|
||||
static const int toUseTheApp = 13;
|
||||
}
|
||||
|
|
@ -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>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(() {});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ Future<void> _tryPushHelper(
|
|||
.first;
|
||||
final event = await client.getEventByPushNotification(
|
||||
notification,
|
||||
storeInDatabase: isBackgroundMessage,
|
||||
storeInDatabase: false,
|
||||
);
|
||||
|
||||
if (event == null) {
|
||||
|
|
|
|||
|
|
@ -191,7 +191,6 @@ class UrlLauncher {
|
|||
await PublicRoomBottomSheet.show(
|
||||
context: context,
|
||||
roomAlias: identityParts.primaryIdentifier,
|
||||
// Pangea#
|
||||
);
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 --")
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
command script import --relative-to-command-file flutter_lldb_helper.py
|
||||
386
pubspec.lock
386
pubspec.lock
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue