Merge main into prod (#5167)

* update lemma meaning and phonetic transcription repos

* chore: simplify progress bar widget

* Remove instructions from chat view, and add profile explanation to course participant page

* Translate courseParticipantTooltip

* fix: in course chats list, sort activities by activity ID

* use different text in chat/course participant tooltips

* depress disabled toolbar buttons

* fix: load course images on course load

* fix: on add course plan to space, set m.space.child power level to 0

* chore: add label to emoji selector in vocab analytics

* chore: increase text sizes in activity summary

* fix: don't show open sessions if user has selected a role

* feat: add button to regenerate latest bot message

* chore: update morph meaning repo

* chore: increase text size and spacing in language selection page, consume language locale emojis

* feat: on first select lemma emoji, show snackbar with explanation

* chore: use builder to style pressable buttons based on height

* chore: add tooltips to each practice mode

* initial work to add shimmer to match activity options

* show word card in image toolbar mode

* use the same widget for word card and vocab details emoji pickers

* add shimmer background to match choices

* fix: close previous snackbar before opening new mode disabled snackbar

* fix: refresh course details when course ID changes in course details

* chore: keep message button depressed

* only show emoji selection shimmer if no emoji is selected

* don't show reaction picker in emoji mode

* lemma emoji picker style updates

* update loading indicators in word zoom card

* feat: show word card in vocab details page

* practice buttons shimmer

* fixed height audio player

* more practice mode updates

* more practice tweaks

* add space between rows of tokens in practice mode

* conditional top padding for practice tooltips

* feat: send message info in lemma info request

* chore: Focus on word meanings in reaction choices

* fix: restrict width of morph icon in practice token button

* chore: Expand word card for meanings

* chore: When first grammar question active, shimmer choices

* chore: Swap seed for hyphen for not-yet-chosen emojis in analytics

* chore: Level attention to emoji and audio icons

* fix: fix non-token vertical spacing in practice mode

* fix: close message overlay when screen size changes

* feat: While audio is playing, allow clicking of word to move audio to that spot

* feat: play audio on token click and on construct click in vocab analytics

* chore: snackbar close button

* feat: Stay in audio mode after end of audio

* chore: more word card spacing adjustments

* fix: use construct id json in route for analytics details page

* feat: custom SSO login/signup dialog

* chore: add content to distinguish system edit from manual edit

* Make input bar suggestion text vertically centered when shrinking

* Add Pangea comments

* Add background to make dark mode icon stand out in own message grammar
practice

* chore: re-style sso popup

* fix: progress bar min width

* fix: change how screen width metric changes are tracked

* simplify

* fix: fix carousel scroll issue

* fix: set emoji placeholder text colot

* fix: when not in column mode, don't add padding to top of practice tooltip

* chore: prevent running IGC on empty message

* fix: allow translation of bot audio transcripts

* feat: analytics database

* fix: update analytics profile room IDs on change, set via parameter in analytics room knock request (#4949)

* chore: center title in add a course page (#4950)

* fix: update spacing of activity participant indicators to make them narrower, make user activity summary highlight row scrollable (#4951)

* fix: remove clicked new token from new tokens cache immeadiatley instead of waiting for new token animation to finish (#4952)

* What now button takes user to top of course plan page (#4946)

* Add scrollController to course details pages

* Make what now button refresh details tab if needed, remove scrollController

* 4907 construct details changes (#4961)

* chore: remove delegation analytics page

* feat: vocab construct analytics level bar

* chore: analytics mobile navigation

* feat: cap construct XP

* Add background to regeneration request background (#4960)

* chore: reduce padding between lines of message in practice mode (#4962)

* chore: don't show message regeneration button if message has already been regenerated (#4963)

* fix: prevent request regeneration button from altering message height (#4964)

* fix: only animate top portion of activity status bar (#4965)

* fix: fix white box error and add opacity variation to construct levels in progress bar (#4966)

* fix: don't close word card on click (#4967)

* feat: after user exits IT three times, show them a popup with the option to disable automatic language assistance (#4968)

* feat: allow token feedback for word card in vocab analytics (#4900)

* feat: allow token feedback for word card in vocab analytics

* fix: remove duplicate global keys

* 4726 word card in arabic goes way to the side (#4970)

* fix: initial work for word card positioning on RTL system

* fix: fix practice mode animation for RTL languages

* chore: close lemma emoji snackbar on parent widget disposed (#4972)

* fix: remove user summary testing code (#4974)

* feat: On hover of the Nav Bar, expand to show current icon tooltip text (#4976)

* feat: On hover of the Nav Bar, expand to show current icon tooltip text

* animate menu transition

* chore: delete construct navigation (#4984)

* chore: Use hyphen instead of seed/sprout/flower in list view (#4985)

* chore: update analytics page on construct update (#4987)

* fix: fix word card overlay in mobile vocab details page (#4988)

* fix: Latest sent message sinks when clicked on Mobile (#4989)

* fix: don't highlight new tokens until analytics initialize (#4990)

* chore: calculate times closed out of IT based on all message in session (#4991)

* chore: add feedback response dialog (#4992)

* chore: move request generation button into message bubble (#4993)

* fix: show request regen button in overlay message (#4996)

* fix: separate block construct and update construct updates in vocab list view (#4998)

* feat: Do gold shimmer every 5 seconds on unselected emojis (#4999)

* simplify message token renderer (#4994)

* simplify message token renderer

* token rendering and new word collection for tokens in activity summary / menu

* make tokens hoverable

* Model key cleanup (#4983)

* refactor: Group redundant ModelKey entries

* Add python script to find and replace hardcoded ModelKey values

* Edited Python script to not automatically use ModelKey for files not
already using it

* refactor: Ran script and accepted obvious changes

* rename 'duration' model key

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>

* fix: return bot local stt, ensure stt rep exists in request stt translation function (#5003)

* chore: set max lines for word card phonetic transcription (#5005)

* chore: Don't show shimmer for unavailable modes (#5006)

* chore: Delay until screen darkening (#5009)

* chore: add focus node to vocab list view search bar (#5011)

* chore: collapse navigation rail on navigate (#5013)

* When user saves course edits, return to details page (#5012)

* fix: don't lowercase construct keys in morph analytics list view (#5014)

* 4860 dms   all chats (#5015)

* feat: initial work for dms => all chats

* more navigation updates

* change all chats tooltip

* fix: set exact reactions length in overlay (#5016)

* fix: fix message list rendering (#5017)

* chore: disable lemma emoji selection for word card in token feedback dialog (#5026)

* fix: don't add XP update if no new construct uses were added (#5027)

* chore: hide request regeneration button in practice mode (#5028)

* chore: use root navigator for chat details dialogs (#5029)

* fix: rebuild word card on new word overlay dismissed (#5030)

* Ensure consistency of pressable button height after animation (#5025)

* Ensure consistency of pressable button height after animation

* Use variable instead of hardcoded value

* fix: fix overlay reactions bouncing around (#5031)

* fix: add horizontal padding to prevent choice animation cutoff (#5032)

* 4919 further optimizing message info (#5033)

* remove original sent from message content

* don't add null fields to message content JSON

* fix: only show disable language assistance popup is user manually closes IT (#5034)

* fix: only exclude xp gained analytics events if blocked constructs has entry (#5035)

* fix: on analytics DB init, don't clear DB unless stored userID doesn't match client userID (#5036)

* don't log missing POS error for POS 'other' (#5039)

* don't long missing POS error for POS 'other'

* don't long error for missing grammar copy if lemma is 'other'

* chore: rebuild input bar hint text on language update (#5042)

* fix: clear database on reinitialize (#5045)

* chore: default to reactions maxWidth null if not available (#5047)

* fix: remove duplicate navigator pop in member actions popup (#5048)

* Reduce gap between lines in practice modes (#5041)

* fix: prevent word card overflow in vocab details (#5049)

* chore: style tokens in transcription like other clickable tokens (#5055)

* fix: always align space nav rail children to the left (#5059)

* chore: update message analytics feedback popup background color (#5061)

* chore: increase padding in span card scroll view to prevent choice animation overflow (#5062)

* chore: Don't use dropdown if only one item (#5063)

* chore: Disable ability to send video/files (slash anything else that the bot doesn’t know what to do with) in bot chats (#5065)

* chore: show more specific error in audio recording dialog (#5068)

* chore: stack expanded space navigation menu over screen in one column mode (#5069)

* feat: when screen size gets too short, show warning dialog (#5070)

* 5053 can get points from lemma with max score (#5078)

* make uses a private field for ConstructUses

* expose capped list of uses in ConstructUses

* filter capped construct uses in getUses

* fix: don't show send button if error in recording dialog (#5079)

* chore: allow users to highlight main word in word card

* fix: in emoji picker, don't set selected emoji based on old stream data

* remove duplicate subscription cancel

* fix: fix recording dialog import error

* fix: disable new token collection for token not in L2

* chore: use activity plan CEFR level in saved activity display

* chore: apply border to dialog directly in delete space dialog (#5093)

* chore: hide nav rail item tooltips when expanded (#5094)

* chore: reduce min height of span card feedback section (#5095)

* chore: force span card to always go above input bar (#5096)

* fix: always enable small screen warning dialog on web (#5097)

* fix: add new blocks to merge table before fetching previous constructs when calculating points added by construct update (#5098)

* fix: remove reaction subscription to prevent overlay jumping (#5100)

* 4825 vocabulary practice (#4826)

* chore: move logic for lastUsedByActivityType into ConstructIdentifier

* feat: vocab practice

* add vocab activity progress bar

* fix: shuffle audio practice choices

* update UI of vocab practice

Added buttons, increased text size and change position, cards flip over and turn red/green on click and respond to hover input

* add xp sparkle, shimmering choice card placeholder

* spacing changes

fix padding, make choice cards spacing/sizing responsive to screen size, replace shimmer cards with stationary circle indicator

* don't include duplicate lemma choices

* use constructID and show lemma/emoji on choice cards

add method to clear cache in case the results was an error, and add a retry button on error

* gain xp immediately and take out continue session

also refactor the choice cards to have separate widgets for each type and a parent widget to give each an id for xp sparkle

* add practice finished page with analytics

* Color tweaks on completed page and time card placeholder

* add timer

* give XP for bonuses and change timer to use stopwatch

* simplify card logic, lock practice when few vocab words

* merge analytics changes and fix bugs

* reload on language change

- derive XP data from new analytics
- Don't allow any clicks after correct answer selected

* small fixes, added tooltip, added copy to l10

* small tweaks and comments

* formatting and import sorting

---------

Co-authored-by: avashilling <165050625+avashilling@users.noreply.github.com>

* feat: Directing to click messages with shimmer (#5106)

* fix: use standard loading dialog on submit delete space dialog (#5107)

* chore: don't show practice tooltip if mode is complete (#5108)

* chore: don't restrict token length (#5112)

* fix: in recording dialog, throw exception on permission denied (#5114)

* chore: remove margin from last entry in user activity summary list (#5115)

* chore: make emoji choice shimmer background match word card background (#5116)

* feat: allow users to update bot's voice settings (#5119)

* fix: hide ability to change bot chat settings from non-admins (#5120)

* fix: remove extra text from end of download file name (#5121)

* fix: remove invalid expanded widget (#5124)

* fix: add guard to prevent showing screen size popup when expanding screen after showing popup (#5127)

* chore: normalize accents in vocab search (#5128)

* chore: base level icon spacing on xp needed to reach level in vocab details (#5131)

* chore: add padding to bottom of vocab list view so practice button doesn't block last vocab entries (#5132)

* fix: fix practice record construct id assignment for morph activities (#5133)

* fix: coerce existing aggregate analytics database entries into correct format before merging to avoid data loss (#5136)

* feat: make construct aggregated case-insensitive (#5137)

* chore: prevent user from spamming disabled vocab practice button (#5138)

* fix: reset voice on langauge update (#5140)

* chore: make emoji base shimmer transparent (#5142)

* chore: update sort order in space participants list (#5144)

* chore: remove padding from last entry in activity list (#5146)

* fix: disable emoji setting for non-L2 constructs (#5148)

* fix: add reaction notifier to rebuild reaction picker and reaction display on reaction change (#5151)

* chore: decrease text sizes in vocab practice complete page in one column mode (#5152)

* chore: hide download button in download dialogs if download is complete (#5157)

* fix: show morph as unlocked in analytics if ever used (#5158)

* chore: reduce span card spacing to reduce unneeded scroll (#5160)

* chore: reduce span card spacing to reduce unneeded scroll

* remove debugging code

* fix: don't double space ID on navigation (#5163)

* chore: reduce negative points to 1 (#5162)

To eliminate the chance of having negative total, minimum upon completion now is 30XP

* fix: remove duplicates from answer choices (#5161)

* fix: use canonical activity time in display for completed activity (#5164)

* chore: refresh language cache to add voices (#5165)

* chore: don't show loading dialog on reaction redaction (#5166)

* build: bump version

---------

Co-authored-by: Kelrap <kel.raphael3@outlook.com>
Co-authored-by: Kelrap <99418823+Kelrap@users.noreply.github.com>
Co-authored-by: avashilling <165050625+avashilling@users.noreply.github.com>
This commit is contained in:
ggurdin 2026-01-12 09:59:12 -05:00 committed by GitHub
parent 1f9b49308d
commit 09e4741adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
626 changed files with 104297 additions and 159988 deletions

7
.gitignore vendored
View file

@ -48,6 +48,9 @@ venv/
/builds/
.fvm/
.fvmrc
# copilot-instructions.md
.github/copilot-instructions.md
# Web related
docs/tailwind.css
@ -78,6 +81,10 @@ olm
needed-translations.txt
.venv
# Generated files from find_unused_intl_keys.py
scripts/unused_intl_keys_report.txt
scripts/unused_intl_keys.json
docs/node_modules/.package-lock.json
docs/node_modules/.bin/detect-libc
docs/node_modules/.bin/jiti

58
.vscode/launch.json vendored
View file

@ -7,65 +7,7 @@
{
"name": "pangea-chat",
"request": "launch",
"type": "dart",
// "args": [
// "-d",
// "chrome",
// "--web-port",
// "49632"
// ],
},
{
"name": "pangea-chat (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "pangea-chat (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "pangea_choreographer",
"cwd": "pangea_packages\\pangea_choreographer",
"request": "launch",
"type": "dart"
},
{
"name": "pangea_choreographer (profile mode)",
"cwd": "pangea_packages\\pangea_choreographer",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "pangea_choreographer (release mode)",
"cwd": "pangea_packages\\pangea_choreographer",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "pangea_language",
"cwd": "pangea_packages\\pangea_language",
"request": "launch",
"type": "dart"
},
{
"name": "pangea_language (profile mode)",
"cwd": "pangea_packages\\pangea_language",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "pangea_language (release mode)",
"cwd": "pangea_packages\\pangea_language",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View file

@ -109,6 +109,20 @@
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="app.pangea.chat" />
<data
android:scheme="https"
android:host="app.staging.pangea.chat" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 KiB

View file

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>

View file

@ -7,6 +7,7 @@
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:app.pangea.chat</string>
<string>applinks:app.staging.pangea.chat</string>
</array>
<key>com.apple.security.application-groups</key>
<array>

View file

@ -27,7 +27,7 @@ abstract class AppConfig {
static const bool allowOtherHomeservers = true;
static const bool enableRegistration = true;
// #Pangea
static const double toolbarMaxHeight = 225.0;
static const double toolbarMaxHeight = 250.0;
static const double toolbarMinHeight = 150.0;
static const double toolbarMinWidth = 350.0;
static const double toolbarMenuHeight = 50.0;
@ -35,7 +35,7 @@ abstract class AppConfig {
static const double toolbarButtonsHeight = 50.0;
static const double toolbarSpacing = 8.0;
static const double toolbarIconSize = 24.0;
static const double readingAssistanceInputBarHeight = 140.0;
static const double readingAssistanceInputBarHeight = 175.0;
static const double reactionsPickerHeight = 48.0;
static const double chatInputRowOverlayPadding = 8.0;
static const double selectModeInputBarHeight = 0;

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
@ -32,18 +33,21 @@ import 'package:fluffychat/pages/settings_password/settings_password.dart';
import 'package:fluffychat/pages/settings_security/settings_security.dart';
import 'package:fluffychat/pages/settings_style/settings_style.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/analytics_navigation_util.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_page/analytics_page.dart';
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
import 'package:fluffychat/pangea/analytics_page/empty_analytics_page.dart';
import 'package:fluffychat/pangea/analytics_summary/level_analytics_details_content.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/chat_settings/pages/edit_course.dart';
import 'package:fluffychat/pangea/chat_settings/pages/pangea_invitation_selection.dart';
import 'package:fluffychat/pangea/common/utils/p_vguard.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/course_creation/course_invite_page.dart';
import 'package:fluffychat/pangea/course_creation/selected_course_page.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people_constants.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import 'package:fluffychat/pangea/join_codes/join_with_link_page.dart';
import 'package:fluffychat/pangea/learning_settings/settings_learning.dart';
import 'package:fluffychat/pangea/login/pages/add_course_page.dart';
import 'package:fluffychat/pangea/login/pages/course_code_page.dart';
import 'package:fluffychat/pangea/login/pages/create_pangea_account_page.dart';
@ -53,9 +57,9 @@ import 'package:fluffychat/pangea/login/pages/new_course_page.dart';
import 'package:fluffychat/pangea/login/pages/public_courses_page.dart';
import 'package:fluffychat/pangea/login/pages/signup.dart';
import 'package:fluffychat/pangea/space_analytics/space_analytics.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/pangea/spaces/utils/join_with_link.dart';
import 'package:fluffychat/pangea/spaces/space_constants.dart';
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.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';
@ -347,7 +351,7 @@ abstract class AppRoutes {
child: CachedNetworkImage(
width: 250.0,
imageUrl:
"${AppConfig.assetsBaseURL}/${FindYourPeopleConstants.sideBearFileName}",
"${AppConfig.assetsBaseURL}/${SpaceConstants.sideBearFileName}",
),
)
// Pangea#
@ -518,11 +522,11 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
AnalyticsPage(
indicator: FluffyThemes.isColumnMode(context)
? null
: ProgressIndicatorEnum.wordsUsed,
),
FluffyThemes.isColumnMode(context)
? const EmptyAnalyticsPage()
: const ConstructAnalyticsView(
view: ConstructTypeEnum.vocab,
),
),
routes: [
GoRoute(
@ -530,26 +534,27 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
AnalyticsPage(
indicator: FluffyThemes.isColumnMode(context)
? null
: ProgressIndicatorEnum.morphsUsed,
),
FluffyThemes.isColumnMode(context)
? const EmptyAnalyticsPage()
: const ConstructAnalyticsView(
view: ConstructTypeEnum.morph,
),
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: ':construct',
pageBuilder: (context, state) {
final construct = ConstructIdentifier.fromString(
state.pathParameters['construct']!,
final construct = ConstructIdentifier.fromJson(
jsonDecode(state.pathParameters['construct']!),
);
return defaultPageBuilder(
context,
state,
AnalyticsPage(
indicator: ProgressIndicatorEnum.morphsUsed,
ConstructAnalyticsView(
construct: construct,
view: ConstructTypeEnum.morph,
),
);
},
@ -561,26 +566,36 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
AnalyticsPage(
indicator: FluffyThemes.isColumnMode(context)
? null
: ProgressIndicatorEnum.wordsUsed,
),
FluffyThemes.isColumnMode(context)
? const EmptyAnalyticsPage()
: const ConstructAnalyticsView(
view: ConstructTypeEnum.vocab,
),
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: 'practice',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
const VocabPractice(),
);
},
),
GoRoute(
path: ':construct',
pageBuilder: (context, state) {
final construct = ConstructIdentifier.fromString(
state.pathParameters['construct']!,
final construct = ConstructIdentifier.fromJson(
jsonDecode(state.pathParameters['construct']!),
);
return defaultPageBuilder(
context,
state,
AnalyticsPage(
indicator: ProgressIndicatorEnum.wordsUsed,
ConstructAnalyticsView(
construct: construct,
view: ConstructTypeEnum.vocab,
),
);
},
@ -592,11 +607,9 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
AnalyticsPage(
indicator: FluffyThemes.isColumnMode(context)
? null
: ProgressIndicatorEnum.activities,
),
FluffyThemes.isColumnMode(context)
? const EmptyAnalyticsPage()
: const ActivityArchive(),
),
redirect: loggedOutRedirect,
routes: [
@ -609,9 +622,12 @@ abstract class AppRoutes {
roomId: state.pathParameters['roomid']!,
eventId: state.uri.queryParameters['event'],
backButton: BackButton(
onPressed: () => context.go(
"/rooms/analytics/activities",
),
onPressed: () {
AnalyticsNavigationUtil.navigateToAnalytics(
context: context,
view: ProgressIndicatorEnum.activities,
);
},
),
),
),
@ -624,11 +640,9 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
AnalyticsPage(
indicator: FluffyThemes.isColumnMode(context)
? null
: ProgressIndicatorEnum.level,
),
FluffyThemes.isColumnMode(context)
? const EmptyAnalyticsPage()
: const LevelAnalyticsDetailsContent(),
),
redirect: loggedOutRedirect,
),
@ -989,30 +1003,7 @@ abstract class AppRoutes {
),
);
},
// #Pangea
// redirect: loggedOutRedirect,
redirect: (context, state) {
String subroute = state.fullPath!.split('roomid').last;
if (state.uri.queryParameters.isNotEmpty) {
final queryString = state.uri.queryParameters.entries
.map((e) => '${e.key}=${e.value}')
.join('&');
subroute = '$subroute?$queryString';
}
final roomId = state.pathParameters['roomid']!;
final room = Matrix.of(context).client.getRoomById(roomId);
if (room != null && room.isSpace) {
return "/rooms/spaces/${room.id}$subroute";
}
final parent = room?.firstSpaceParent;
if (parent != null && state.fullPath != null) {
return "/rooms/spaces/${parent.id}/$roomId$subroute";
}
return loggedOutRedirect(context, state);
},
// Pangea#
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: 'search',

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,8 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/learning_settings/utils/locale_provider.dart';
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
import 'package:fluffychat/pangea/languages/locale_provider.dart';
import 'package:fluffychat/pangea/languages/p_language_store.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'config/setting_keys.dart';
@ -42,7 +42,12 @@ void main() async {
/// Then where ever you need language functions simply call PangeaLanguage pangeaLanguage = PangeaLanguage()
/// pangeaLanguage.getList or whatever function you need
///
await GetStorage.init();
final List<Future> initFutures = [
GetStorage.init(),
GetStorage.init("subscription_storage"),
GetStorage.init('class_storage'),
];
await Future.wait(initFutures);
// Pangea#
// Our background push shared isolate accesses flutter-internal things very early in the startup proccess

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart';
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/sync_status_localization.dart';
@ -30,23 +28,6 @@ class ChatAppBarTitle extends StatelessWidget {
// ),
// );
// }
if (controller.room.showActivityChatUI) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.room.getLocalizedDisplayname(),
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
ActivityStatsButton(controller: controller),
],
),
);
}
// Pangea#
return InkWell(
hoverColor: Colors.transparent,
@ -56,7 +37,14 @@ class ChatAppBarTitle extends StatelessWidget {
? null
: () => FluffyThemes.isThreeColumnMode(context)
? controller.toggleDisplayChatDetailsColumn()
: context.go('/rooms/${room.id}/details'),
// #Pangea
// : context.go('/rooms/${room.id}/details'),
: NavigationUtil.goToSpaceRoute(
room.id,
['details'],
context,
),
// Pangea#
child: Row(
children: [
Hero(

View file

@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/sticker_picker_dialog.dart';
import 'chat.dart';
class ChatEmojiPicker extends StatelessWidget {
@ -25,13 +23,18 @@ class ChatEmojiPicker extends StatelessWidget {
: 0,
child: controller.showEmojiPicker
? DefaultTabController(
length: 2,
// #Pangea
// length: 2,
length: 1,
// Pangea#
child: Column(
children: [
TabBar(
tabs: [
Tab(text: L10n.of(context).emojis),
Tab(text: L10n.of(context).stickers),
// #Pangea
// Tab(text: L10n.of(context).stickers),
// Pangea#
],
),
Expanded(
@ -67,20 +70,22 @@ class ChatEmojiPicker extends StatelessWidget {
),
),
),
StickerPickerDialog(
room: controller.room,
onSelected: (sticker) {
controller.room.sendEvent(
{
'body': sticker.body,
'info': sticker.info ?? {},
'url': sticker.url.toString(),
},
type: EventTypes.Sticker,
);
controller.hideEmojiPicker();
},
),
// #Pangea
// StickerPickerDialog(
// room: controller.room,
// onSelected: (sticker) {
// controller.room.sendEvent(
// {
// 'body': sticker.body,
// 'info': sticker.info ?? {},
// 'url': sticker.url.toString(),
// },
// type: EventTypes.Sticker,
// );
// controller.hideEmojiPicker();
// },
// ),
// Pangea#
],
),
),

View file

@ -167,6 +167,7 @@ class ChatEventList extends StatelessWidget {
// #Pangea
controller: controller,
isButton: event.eventId == controller.buttonEventID,
canRefresh: event.eventId == controller.refreshEventID,
// Pangea#
selected: controller.selectedEvents
.any((e) => e.eventId == event.eventId),

View file

@ -1,406 +1,394 @@
import 'package:flutter/material.dart';
// import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:matrix/matrix.dart';
// import 'package:animations/animations.dart';
// import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/utils/other_party_can_receive.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../config/themes.dart';
import 'chat.dart';
import 'input_bar.dart';
// import 'package:fluffychat/config/app_config.dart';
// import 'package:fluffychat/l10n/l10n.dart';
// import 'package:fluffychat/utils/other_party_can_receive.dart';
// import 'package:fluffychat/utils/platform_infos.dart';
// import 'package:fluffychat/widgets/avatar.dart';
// import 'package:fluffychat/widgets/matrix.dart';
// import '../../config/themes.dart';
// import 'chat.dart';
// import 'input_bar.dart';
class ChatInputRow extends StatelessWidget {
final ChatController controller;
// class ChatInputRow extends StatelessWidget {
// final ChatController controller;
const ChatInputRow(this.controller, {super.key});
// const ChatInputRow(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// @override
// Widget build(BuildContext context) {
// final theme = Theme.of(context);
const height = 48.0;
// const height = 48.0;
if (!controller.room.otherPartyCanReceiveMessages) {
return Center(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context).otherPartyNotLoggedIn,
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
),
);
}
// if (!controller.room.otherPartyCanReceiveMessages) {
// return Center(
// child: Padding(
// padding: const EdgeInsets.all(12.0),
// child: Text(
// L10n.of(context).otherPartyNotLoggedIn,
// style: theme.textTheme.bodySmall,
// textAlign: TextAlign.center,
// ),
// ),
// );
// }
final selectedTextButtonStyle = TextButton.styleFrom(
foregroundColor: theme.colorScheme.onTertiaryContainer,
);
// final selectedTextButtonStyle = TextButton.styleFrom(
// foregroundColor: theme.colorScheme.onTertiaryContainer,
// );
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: controller.selectMode
? <Widget>[
if (controller.selectedEvents
.every((event) => event.status == EventStatus.error))
SizedBox(
height: height,
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.orange,
),
onPressed: controller.deleteErrorEventsAction,
child: Row(
children: <Widget>[
const Icon(Icons.delete),
Text(L10n.of(context).delete),
],
),
),
)
else
SizedBox(
height: height,
child: TextButton(
style: selectedTextButtonStyle,
onPressed: controller.forwardEventsAction,
child: Row(
children: <Widget>[
const Icon(Icons.keyboard_arrow_left_outlined),
Text(L10n.of(context).forward),
],
),
),
),
controller.selectedEvents.length == 1
? controller.selectedEvents.first
.getDisplayEvent(controller.timeline!)
.status
.isSent
? SizedBox(
height: height,
child: TextButton(
style: selectedTextButtonStyle,
onPressed: controller.replyAction,
child: Row(
children: <Widget>[
Text(L10n.of(context).reply),
const Icon(Icons.keyboard_arrow_right),
],
),
),
)
: SizedBox(
height: height,
child: TextButton(
style: selectedTextButtonStyle,
onPressed: controller.sendAgainAction,
child: Row(
children: <Widget>[
Text(L10n.of(context).tryToSendAgain),
const SizedBox(width: 4),
const Icon(Icons.send_outlined, size: 16),
],
),
),
)
: const SizedBox.shrink(),
]
: <Widget>[
const SizedBox(width: 4),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
alignment: Alignment.center,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton<String>(
useRootNavigator: true,
icon: const Icon(Icons.add_circle_outline),
iconColor: theme.colorScheme.onPrimaryContainer,
onSelected: controller.onAddPopupMenuButtonSelected,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'location',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.gps_fixed_outlined),
),
title: Text(L10n.of(context).shareLocation),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'image',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.photo_outlined),
),
title: Text(L10n.of(context).sendImage),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'video',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.video_camera_back_outlined),
),
title: Text(L10n.of(context).sendVideo),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'file',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.attachment_outlined),
),
title: Text(L10n.of(context).sendFile),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
),
if (PlatformInfos.isMobile)
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
alignment: Alignment.center,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton(
useRootNavigator: true,
icon: const Icon(Icons.camera_alt_outlined),
onSelected: controller.onAddPopupMenuButtonSelected,
iconColor: theme.colorScheme.onPrimaryContainer,
itemBuilder: (context) => [
PopupMenuItem<String>(
value: 'camera-video',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.videocam_outlined),
),
title: Text(L10n.of(context).recordAVideo),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'camera',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.camera_alt_outlined),
),
title: Text(L10n.of(context).takeAPhoto),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
),
Container(
height: height,
width: height,
alignment: Alignment.center,
child: IconButton(
tooltip: L10n.of(context).emojis,
color: theme.colorScheme.onPrimaryContainer,
icon: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.scaled,
fillColor: Colors.transparent,
child: child,
);
},
child: Icon(
controller.showEmojiPicker
? Icons.keyboard
: Icons.add_reaction_outlined,
key: ValueKey(controller.showEmojiPicker),
),
),
onPressed: controller.emojiPickerAction,
),
),
if (Matrix.of(context).isMultiAccount &&
Matrix.of(context).hasComplexBundles &&
Matrix.of(context).currentBundle!.length > 1)
Container(
width: height,
height: height,
alignment: Alignment.center,
child: _ChatAccountPicker(controller),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0),
child: InputBar(
room: controller.room,
minLines: 1,
maxLines: 8,
autofocus: !PlatformInfos.isMobile,
keyboardType: TextInputType.multiline,
textInputAction:
AppConfig.sendOnEnter == true && PlatformInfos.isMobile
? TextInputAction.send
: null,
// #Pangea
// onSubmitted: controller.onInputBarSubmitted,
onSubmitted: (_) =>
controller.onInputBarSubmitted(_, context),
// Pangea#
onSubmitImage: controller.sendImageFromClipBoard,
focusNode: controller.inputFocus,
controller: controller.sendController,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
bottom: 6.0,
top: 3.0,
),
hintText: L10n.of(context).writeAMessage,
hintMaxLines: 1,
border: InputBorder.none,
enabledBorder: InputBorder.none,
filled: false,
),
onChanged: controller.onInputBarChanged,
// #Pangea
hintText: "",
// Pangea#
),
),
),
Container(
height: height,
width: height,
alignment: Alignment.center,
child: PlatformInfos.platformCanRecord &&
controller.sendController.text.isEmpty
? FloatingActionButton.small(
tooltip: L10n.of(context).voiceMessage,
onPressed: controller.voiceMessageAction,
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(height),
),
backgroundColor: theme.bubbleColor,
foregroundColor: theme.onBubbleColor,
child: const Icon(Icons.mic_none_outlined),
)
: FloatingActionButton.small(
tooltip: L10n.of(context).send,
// #Pangea
// onPressed: controller.send,
onPressed: () => controller.send(
message: controller.sendController.text,
),
// Pangea#
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(height),
),
backgroundColor: theme.bubbleColor,
foregroundColor: theme.onBubbleColor,
child: const Icon(Icons.send_outlined),
),
),
],
);
}
}
// return Row(
// crossAxisAlignment: CrossAxisAlignment.end,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: controller.selectMode
// ? <Widget>[
// if (controller.selectedEvents
// .every((event) => event.status == EventStatus.error))
// SizedBox(
// height: height,
// child: TextButton(
// style: TextButton.styleFrom(
// foregroundColor: Colors.orange,
// ),
// onPressed: controller.deleteErrorEventsAction,
// child: Row(
// children: <Widget>[
// const Icon(Icons.delete),
// Text(L10n.of(context).delete),
// ],
// ),
// ),
// )
// else
// SizedBox(
// height: height,
// child: TextButton(
// style: selectedTextButtonStyle,
// onPressed: controller.forwardEventsAction,
// child: Row(
// children: <Widget>[
// const Icon(Icons.keyboard_arrow_left_outlined),
// Text(L10n.of(context).forward),
// ],
// ),
// ),
// ),
// controller.selectedEvents.length == 1
// ? controller.selectedEvents.first
// .getDisplayEvent(controller.timeline!)
// .status
// .isSent
// ? SizedBox(
// height: height,
// child: TextButton(
// style: selectedTextButtonStyle,
// onPressed: controller.replyAction,
// child: Row(
// children: <Widget>[
// Text(L10n.of(context).reply),
// const Icon(Icons.keyboard_arrow_right),
// ],
// ),
// ),
// )
// : SizedBox(
// height: height,
// child: TextButton(
// style: selectedTextButtonStyle,
// onPressed: controller.sendAgainAction,
// child: Row(
// children: <Widget>[
// Text(L10n.of(context).tryToSendAgain),
// const SizedBox(width: 4),
// const Icon(Icons.send_outlined, size: 16),
// ],
// ),
// ),
// )
// : const SizedBox.shrink(),
// ]
// : <Widget>[
// const SizedBox(width: 4),
// AnimatedContainer(
// duration: FluffyThemes.animationDuration,
// curve: FluffyThemes.animationCurve,
// width: controller.sendController.text.isNotEmpty ? 0 : height,
// height: height,
// alignment: Alignment.center,
// decoration: const BoxDecoration(),
// clipBehavior: Clip.hardEdge,
// child: PopupMenuButton<String>(
// useRootNavigator: true,
// icon: const Icon(Icons.add_circle_outline),
// iconColor: theme.colorScheme.onPrimaryContainer,
// onSelected: controller.onAddPopupMenuButtonSelected,
// itemBuilder: (BuildContext context) =>
// <PopupMenuEntry<String>>[
// if (PlatformInfos.isMobile)
// PopupMenuItem<String>(
// value: 'location',
// child: ListTile(
// leading: CircleAvatar(
// backgroundColor:
// theme.colorScheme.onPrimaryContainer,
// foregroundColor: theme.colorScheme.primaryContainer,
// child: const Icon(Icons.gps_fixed_outlined),
// ),
// title: Text(L10n.of(context).shareLocation),
// contentPadding: const EdgeInsets.all(0),
// ),
// ),
// PopupMenuItem<String>(
// value: 'image',
// child: ListTile(
// leading: CircleAvatar(
// backgroundColor: theme.colorScheme.onPrimaryContainer,
// foregroundColor: theme.colorScheme.primaryContainer,
// child: const Icon(Icons.photo_outlined),
// ),
// title: Text(L10n.of(context).sendImage),
// contentPadding: const EdgeInsets.all(0),
// ),
// ),
// PopupMenuItem<String>(
// value: 'video',
// child: ListTile(
// leading: CircleAvatar(
// backgroundColor: theme.colorScheme.onPrimaryContainer,
// foregroundColor: theme.colorScheme.primaryContainer,
// child: const Icon(Icons.video_camera_back_outlined),
// ),
// title: Text(L10n.of(context).sendVideo),
// contentPadding: const EdgeInsets.all(0),
// ),
// ),
// PopupMenuItem<String>(
// value: 'file',
// child: ListTile(
// leading: CircleAvatar(
// backgroundColor: theme.colorScheme.onPrimaryContainer,
// foregroundColor: theme.colorScheme.primaryContainer,
// child: const Icon(Icons.attachment_outlined),
// ),
// title: Text(L10n.of(context).sendFile),
// contentPadding: const EdgeInsets.all(0),
// ),
// ),
// ],
// ),
// ),
// if (PlatformInfos.isMobile)
// AnimatedContainer(
// duration: FluffyThemes.animationDuration,
// curve: FluffyThemes.animationCurve,
// width: controller.sendController.text.isNotEmpty ? 0 : height,
// height: height,
// alignment: Alignment.center,
// decoration: const BoxDecoration(),
// clipBehavior: Clip.hardEdge,
// child: PopupMenuButton(
// useRootNavigator: true,
// icon: const Icon(Icons.camera_alt_outlined),
// onSelected: controller.onAddPopupMenuButtonSelected,
// iconColor: theme.colorScheme.onPrimaryContainer,
// itemBuilder: (context) => [
// PopupMenuItem<String>(
// value: 'camera-video',
// child: ListTile(
// leading: CircleAvatar(
// backgroundColor:
// theme.colorScheme.onPrimaryContainer,
// foregroundColor: theme.colorScheme.primaryContainer,
// child: const Icon(Icons.videocam_outlined),
// ),
// title: Text(L10n.of(context).recordAVideo),
// contentPadding: const EdgeInsets.all(0),
// ),
// ),
// PopupMenuItem<String>(
// value: 'camera',
// child: ListTile(
// leading: CircleAvatar(
// backgroundColor:
// theme.colorScheme.onPrimaryContainer,
// foregroundColor: theme.colorScheme.primaryContainer,
// child: const Icon(Icons.camera_alt_outlined),
// ),
// title: Text(L10n.of(context).takeAPhoto),
// contentPadding: const EdgeInsets.all(0),
// ),
// ),
// ],
// ),
// ),
// Container(
// height: height,
// width: height,
// alignment: Alignment.center,
// child: IconButton(
// tooltip: L10n.of(context).emojis,
// color: theme.colorScheme.onPrimaryContainer,
// icon: PageTransitionSwitcher(
// transitionBuilder: (
// Widget child,
// Animation<double> primaryAnimation,
// Animation<double> secondaryAnimation,
// ) {
// return SharedAxisTransition(
// animation: primaryAnimation,
// secondaryAnimation: secondaryAnimation,
// transitionType: SharedAxisTransitionType.scaled,
// fillColor: Colors.transparent,
// child: child,
// );
// },
// child: Icon(
// controller.showEmojiPicker
// ? Icons.keyboard
// : Icons.add_reaction_outlined,
// key: ValueKey(controller.showEmojiPicker),
// ),
// ),
// onPressed: controller.emojiPickerAction,
// ),
// ),
// if (Matrix.of(context).isMultiAccount &&
// Matrix.of(context).hasComplexBundles &&
// Matrix.of(context).currentBundle!.length > 1)
// Container(
// width: height,
// height: height,
// alignment: Alignment.center,
// child: _ChatAccountPicker(controller),
// ),
// Expanded(
// child: Padding(
// padding: const EdgeInsets.symmetric(vertical: 0.0),
// child: InputBar(
// room: controller.room,
// minLines: 1,
// maxLines: 8,
// autofocus: !PlatformInfos.isMobile,
// keyboardType: TextInputType.multiline,
// textInputAction:
// AppConfig.sendOnEnter == true && PlatformInfos.isMobile
// ? TextInputAction.send
// : null,
// onSubmitted: controller.onInputBarSubmitted,
// onSubmitImage: controller.sendImageFromClipBoard,
// focusNode: controller.inputFocus,
// controller: controller.sendController,
// decoration: InputDecoration(
// contentPadding: const EdgeInsets.only(
// left: 6.0,
// right: 6.0,
// bottom: 6.0,
// top: 3.0,
// ),
// hintText: L10n.of(context).writeAMessage,
// hintMaxLines: 1,
// border: InputBorder.none,
// enabledBorder: InputBorder.none,
// filled: false,
// ),
// onChanged: controller.onInputBarChanged,
// ),
// ),
// ),
// Container(
// height: height,
// width: height,
// alignment: Alignment.center,
// child: PlatformInfos.platformCanRecord &&
// controller.sendController.text.isEmpty
// ? FloatingActionButton.small(
// tooltip: L10n.of(context).voiceMessage,
// onPressed: controller.voiceMessageAction,
// elevation: 0,
// heroTag: null,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(height),
// ),
// backgroundColor: theme.bubbleColor,
// foregroundColor: theme.onBubbleColor,
// child: const Icon(Icons.mic_none_outlined),
// )
// : FloatingActionButton.small(
// tooltip: L10n.of(context).send,
// onPressed: controller.send,
// elevation: 0,
// heroTag: null,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(height),
// ),
// backgroundColor: theme.bubbleColor,
// foregroundColor: theme.onBubbleColor,
// child: const Icon(Icons.send_outlined),
// ),
// ),
// ],
// );
// }
// }
class _ChatAccountPicker extends StatelessWidget {
final ChatController controller;
// class _ChatAccountPicker extends StatelessWidget {
// final ChatController controller;
const _ChatAccountPicker(this.controller);
// const _ChatAccountPicker(this.controller);
void _popupMenuButtonSelected(String mxid, BuildContext context) {
final client = Matrix.of(context)
.currentBundle!
.firstWhere((cl) => cl!.userID == mxid, orElse: () => null);
if (client == null) {
Logs().w('Attempted to switch to a non-existing client $mxid');
return;
}
controller.setSendingClient(client);
}
// void _popupMenuButtonSelected(String mxid, BuildContext context) {
// final client = Matrix.of(context)
// .currentBundle!
// .firstWhere((cl) => cl!.userID == mxid, orElse: () => null);
// if (client == null) {
// Logs().w('Attempted to switch to a non-existing client $mxid');
// return;
// }
// controller.setSendingClient(client);
// }
@override
Widget build(BuildContext context) {
final clients = controller.currentRoomBundle;
return Padding(
padding: const EdgeInsets.all(8.0),
child: FutureBuilder<Profile>(
future: controller.sendingClient.fetchOwnProfile(),
builder: (context, snapshot) => PopupMenuButton<String>(
useRootNavigator: true,
onSelected: (mxid) => _popupMenuButtonSelected(mxid, context),
itemBuilder: (BuildContext context) => clients
.map(
(client) => PopupMenuItem<String>(
value: client!.userID,
child: FutureBuilder<Profile>(
future: client.fetchOwnProfile(),
builder: (context, snapshot) => ListTile(
leading: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
client.userID!.localpart,
size: 20,
),
title: Text(snapshot.data?.displayName ?? client.userID!),
contentPadding: const EdgeInsets.all(0),
),
),
),
)
.toList(),
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
Matrix.of(context).client.userID!.localpart,
size: 20,
),
),
),
);
}
}
// @override
// Widget build(BuildContext context) {
// final clients = controller.currentRoomBundle;
// return Padding(
// padding: const EdgeInsets.all(8.0),
// child: FutureBuilder<Profile>(
// future: controller.sendingClient.fetchOwnProfile(),
// builder: (context, snapshot) => PopupMenuButton<String>(
// useRootNavigator: true,
// onSelected: (mxid) => _popupMenuButtonSelected(mxid, context),
// itemBuilder: (BuildContext context) => clients
// .map(
// (client) => PopupMenuItem<String>(
// value: client!.userID,
// child: FutureBuilder<Profile>(
// future: client.fetchOwnProfile(),
// builder: (context, snapshot) => ListTile(
// leading: Avatar(
// mxContent: snapshot.data?.avatarUrl,
// name: snapshot.data?.displayName ??
// client.userID!.localpart,
// size: 20,
// ),
// title: Text(snapshot.data?.displayName ?? client.userID!),
// contentPadding: const EdgeInsets.all(0),
// ),
// ),
// ),
// )
// .toList(),
// child: Avatar(
// mxContent: snapshot.data?.avatarUrl,
// name: snapshot.data?.displayName ??
// Matrix.of(context).client.userID!.localpart,
// size: 20,
// ),
// ),
// ),
// );
// }
// }

View file

@ -15,12 +15,16 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart';
import 'package:fluffychat/pages/chat/pinned_events.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_menu_button.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_session_popup_menu.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/load_activity_summary_widget.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart';
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_floating_action_button.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -122,47 +126,68 @@ class ChatView extends StatelessWidget {
// ],
// ),
// ];
// } else
// Pangea#
if (!controller.room.isArchived) {
// #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 [];
if (controller.room.isArchived || controller.room.hasArchivedActivity) {
return [];
}
if (controller.room.showActivityChatUI) {
return [
if (controller.room.activityPlan == null ||
!controller.room.showActivityChatUI)
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');
}
},
ActivityMenuButton(controller: controller),
ActivitySessionPopupMenu(
controller.room,
onLeave: controller.onLeave,
),
];
// 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#
}
return [];
return [
IconButton(
icon: const Icon(Icons.search_outlined),
tooltip: L10n.of(context).search,
onPressed: () {
NavigationUtil.goToSpaceRoute(
controller.room.id,
['search'],
context,
);
},
),
IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: L10n.of(context).chatDetails,
onPressed: () {
if (GoRouterState.of(context).uri.path.endsWith('/details')) {
NavigationUtil.goToSpaceRoute(
controller.room.id,
[],
context,
);
} else {
NavigationUtil.goToSpaceRoute(
controller.room.id,
['details'],
context,
);
}
},
),
];
// Pangea#
}
@override
@ -197,6 +222,16 @@ class ChatView extends StatelessWidget {
builder: (context, snapshot) => FutureBuilder(
future: controller.loadTimelineFuture,
builder: (BuildContext context, snapshot) {
// #Pangea
if (controller.room.isActivitySession &&
!controller.room.isActivityStarted) {
return ActivitySessionStartPage(
activityId: controller.room.activityId!,
roomId: controller.roomId,
parentId: controller.room.courseParent?.id,
);
}
// Pangea#
var appbarBottomHeight = 0.0;
if (controller.room.pinnedEventIds.isNotEmpty) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
@ -207,9 +242,7 @@ class ChatView extends StatelessWidget {
return Scaffold(
appBar: AppBar(
// #Pangea
// actionsIconTheme:
// IconThemeData(
// #Pangea
// actionsIconTheme: IconThemeData(
// color: controller.selectedEvents.isEmpty
// ? null
// : theme.colorScheme.onTertiaryContainer,
@ -217,9 +250,6 @@ class ChatView extends StatelessWidget {
// backgroundColor: controller.selectedEvents.isEmpty
// ? null
// : theme.colorScheme.tertiaryContainer,
toolbarHeight:
controller.room.showActivityChatUI ? 106.0 : null,
centerTitle: controller.room.showActivityChatUI,
// Pangea#
automaticallyImplyLeading: false,
leading: controller.selectMode
@ -242,20 +272,25 @@ class ChatView extends StatelessWidget {
),
// #Pangea
// builder: (context, _) => UnreadRoomsBadge(
// filter: (r) => r.id != controller.roomId,
// badgePosition:
// BadgePosition.topEnd(end: 8, top: 4),
// child: const Center(child: BackButton()),
// ),
builder: (context, _) => Center(
child: SizedBox(
height: kToolbarHeight,
child: UnreadRoomsBadge(
// Pangea#
filter: (r) => r.id != controller.roomId,
badgePosition: BadgePosition.topEnd(
end: 8,
top: 4,
top: 9,
),
child: const Center(child: BackButton()),
),
),
),
// Pangea#
),
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
title: ChatAppBarTitle(controller),
@ -265,13 +300,6 @@ class ChatView extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// #Pangea
if (!controller.showActivityDropdown)
Divider(
height: 1,
color: theme.dividerColor,
),
// Pangea#
PinnedEvents(controller),
if (scrollUpBannerEventId != null)
ChatAppBarListTile(
@ -316,214 +344,213 @@ class ChatView extends StatelessWidget {
// ),
// )
// : null,
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 56.0),
child: ChatFloatingActionButton(controller: controller),
),
// body: DropTarget(
// onDragDone: controller.onDragDone,
// onDragEntered: controller.onDragEntered,
// onDragExited: controller.onDragExited,
// child: Stack(
body: Stack(
// Pangea#
children: <Widget>[
if (accountConfig.wallpaperUrl != null)
Opacity(
opacity: accountConfig.wallpaperOpacity ?? 0.5,
child: ImageFiltered(
imageFilter: ui.ImageFilter.blur(
sigmaX: accountConfig.wallpaperBlur ?? 0.0,
sigmaY: accountConfig.wallpaperBlur ?? 0.0,
),
child: MxcImage(
cacheKey: accountConfig.wallpaperUrl.toString(),
uri: accountConfig.wallpaperUrl,
fit: BoxFit.cover,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
isThumbnail: false,
placeholder: (_) => Container(),
body: SafeArea(
child: Stack(
// Pangea#
children: <Widget>[
if (accountConfig.wallpaperUrl != null)
Opacity(
opacity: accountConfig.wallpaperOpacity ?? 0.5,
child: ImageFiltered(
imageFilter: ui.ImageFilter.blur(
sigmaX: accountConfig.wallpaperBlur ?? 0.0,
sigmaY: accountConfig.wallpaperBlur ?? 0.0,
),
child: MxcImage(
cacheKey: accountConfig.wallpaperUrl.toString(),
uri: accountConfig.wallpaperUrl,
fit: BoxFit.cover,
height: MediaQuery.sizeOf(context).height,
width: MediaQuery.sizeOf(context).width,
isThumbnail: false,
placeholder: (_) => Container(),
),
),
),
),
SafeArea(
// #Pangea
// child: Column(
child: Stack(
children: [
Column(
// Pangea#
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: controller.clearSingleSelectedEvent,
child: ChatEventList(controller: controller),
),
),
// #Pangea
// if (controller.showScrollDownButton)
// Divider(
// height: 1,
// color: theme.dividerColor,
// ),
// Pangea#
if (controller.room.isExtinct)
Container(
margin: EdgeInsets.all(bottomSheetPadding),
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.chevron_right),
label: Text(L10n.of(context).enterNewChat),
onPressed: controller.goToNewRoomAction,
),
)
// #Pangea
// else if (controller.room.canSendDefaultMessages &&
// controller.room.membership == Membership.join)
else if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join &&
controller.room.isAbandonedDMRoom == true)
// Pangea#
Container(
margin: EdgeInsets.all(bottomSheetPadding),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.maxTimelineWidth,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: controller.selectedEvents.isNotEmpty
? theme.colorScheme.tertiaryContainer
: theme.colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(
Radius.circular(24),
SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: GestureDetector(
// #Pangea
// onTap: controller.clearSingleSelectedEvent,
// child: ChatEventList(controller: controller),
child: Stack(
children: [
ListenableBuilder(
listenable:
controller.timelineUpdateNotifier,
builder: (context, _) {
return ChatEventList(
controller: controller,
);
},
),
child: controller.room.isAbandonedDMRoom ==
true
? Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
foregroundColor:
theme.colorScheme.error,
),
icon: const Icon(
Icons.archive_outlined,
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context).leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed:
controller.recreateChat,
label: Text(
L10n.of(context).reopenChat,
),
),
],
)
// #Pangea
: null,
// : Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// ReplyDisplay(controller),
// ChatInputRow(controller),
// ChatEmojiPicker(controller),
// ],
// ),
// Pangea#
),
ChatViewBackground(
controller.choreographer.itController.open,
),
],
),
// #Pangea
// Keep messages above minimum input bar height
if (!controller.room.isAbandonedDMRoom &&
controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join &&
(controller.room.activityPlan == null ||
!controller.room.showActivityChatUI ||
controller.room.isActiveInActivity))
AnimatedSize(
duration: const Duration(milliseconds: 200),
child: SizedBox(
height: controller.inputBarHeight +
bottomSheetPadding,
),
// Pangea#
),
),
// #Pangea
// if (controller.showScrollDownButton)
// Divider(
// height: 1,
// color: theme.dividerColor,
// ),
ListenableBuilder(
listenable: controller.scrollController,
builder: (context, _) {
if (controller.scrollController.hasClients &&
controller.scrollController.position.pixels >
0) {
return Divider(
height: 1,
color: theme.dividerColor,
);
} else {
return const SizedBox.shrink();
}
},
),
// Pangea#
if (controller.room.isExtinct)
Container(
margin: EdgeInsets.all(bottomSheetPadding),
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.chevron_right),
label: Text(L10n.of(context).enterNewChat),
onPressed: controller.goToNewRoomAction,
),
if (controller.room.isActivityFinished)
LoadActivitySummaryWidget(
room: controller.room,
),
)
// #Pangea
// else if (controller.room.canSendDefaultMessages &&
// controller.room.membership == Membership.join)
// Container(
// margin: EdgeInsets.all(bottomSheetPadding),
// constraints: const BoxConstraints(
// maxWidth: FluffyThemes.maxTimelineWidth,
// ),
// alignment: Alignment.center,
// child: Material(
// clipBehavior: Clip.hardEdge,
// color: controller.selectedEvents.isNotEmpty
// ? theme.colorScheme.tertiaryContainer
// : theme.colorScheme.surfaceContainerHigh,
// borderRadius: const BorderRadius.all(
// Radius.circular(24),
// ),
// child: controller.room.isAbandonedDMRoom == true
// ? Row(
// mainAxisAlignment:
// MainAxisAlignment.spaceEvenly,
// children: [
// TextButton.icon(
// style: TextButton.styleFrom(
// padding: const EdgeInsets.all(
// 16,
// ),
// foregroundColor:
// theme.colorScheme.error,
// ),
// icon: const Icon(
// Icons.archive_outlined,
// ),
// onPressed: controller.leaveChat,
// label: Text(
// L10n.of(context).leave,
// ),
// ),
// TextButton.icon(
// style: TextButton.styleFrom(
// padding: const EdgeInsets.all(
// 16,
// ),
// ),
// icon: const Icon(
// Icons.forum_outlined,
// ),
// onPressed: controller.recreateChat,
// label: Text(
// L10n.of(context).reopenChat,
// ),
// ),
// ],
// )
// : Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// ReplyDisplay(controller),
// ChatInputRow(controller),
// ChatEmojiPicker(controller),
// ],
// ),
// ),
// )
else if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join &&
(controller.room.activityPlan == null ||
!controller.room.showActivityChatUI ||
controller.room.isActiveInActivity))
ChatInputBar(
controller: controller,
padding: bottomSheetPadding,
)
else if (controller.room.activityPlan != null &&
controller.room.showActivityChatUI &&
!controller.room.isActiveInActivity)
ActivityFinishedStatusMessage(
controller: controller,
),
// Pangea#
],
),
// #Pangea
ChatViewBackground(controller.choreographer),
if (!controller.room.isAbandonedDMRoom &&
controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join &&
(controller.room.activityPlan == null ||
!controller.room.showActivityChatUI ||
controller.room.isActiveInActivity))
Positioned(
left: 0,
right: 0,
bottom: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ChatInputBarHeader(
controller: controller,
padding: bottomSheetPadding,
),
ChatInputBar(
controller: controller,
padding: bottomSheetPadding,
),
],
if (controller.room.isActivityFinished)
LoadActivitySummaryWidget(
room: controller.room,
),
),
ActivityStatsMenu(controller),
if (controller.room.activitySummary?.summary != null &&
controller.hasRainedConfetti == false)
StarRainWidget(
showBlast: true,
onFinished: () =>
controller.setHasRainedConfetti(true),
),
// Pangea#
],
// Pangea#
],
),
),
),
// #Pangea
// if (controller.dragging)
// Container(
// color: theme.scaffoldBackgroundColor.withAlpha(230),
// alignment: Alignment.center,
// child: const Icon(
// Icons.upload_outlined,
// size: 100,
// ),
// ),
// Pangea#
],
// #Pangea
ActivityStatsMenu(controller),
if (controller.room.activitySummary?.summary != null)
ValueListenableBuilder(
valueListenable:
controller.activityController.hasRainedConfetti,
builder: (context, hasRained, __) {
return hasRained
? const SizedBox()
: StarRainWidget(
showBlast: true,
onFinished: () => controller
.activityController
.setHasRainedConfetti(true),
);
},
),
// if (controller.dragging)
// Container(
// color: theme.scaffoldBackgroundColor.withAlpha(230),
// alignment: Alignment.center,
// child: const Icon(
// Icons.upload_outlined,
// size: 100,
// ),
// ),
// Pangea#
],
),
),
);
},

View file

@ -14,9 +14,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
@ -35,9 +33,8 @@ class AudioPlayerWidget extends StatefulWidget {
final String roomId;
final String senderId;
final PangeaAudioFile? matrixFile;
final ChatController chatController;
final MessageOverlayController? overlayController;
final bool autoplay;
final bool enableClicks;
// Pangea#
static const int wavesCount = 40;
@ -52,9 +49,8 @@ class AudioPlayerWidget extends StatefulWidget {
required this.roomId,
required this.senderId,
this.matrixFile,
required this.chatController,
this.overlayController,
this.autoplay = false,
this.enableClicks = true,
// Pangea#
super.key,
});
@ -75,7 +71,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
String? _durationString;
// #Pangea
StreamSubscription? _onAudioPositionChanged;
StreamSubscription? _onAudioStateChanged;
double playbackSpeed = 1.0;
@ -157,7 +152,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
audioPlayer.dispose();
matrix.voiceMessageEventId.value = matrix.audioPlayer = null;
// #Pangea
_onAudioPositionChanged?.cancel();
_onAudioStateChanged?.cancel();
// Pangea#
}
@ -265,18 +259,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
// #Pangea
audioPlayer.setSpeed(playbackSpeed);
_onAudioPositionChanged?.cancel();
_onAudioPositionChanged =
matrix.audioPlayer!.positionStream.listen((state) {
// Pass current timestamp to overlay, so it can highlight as necessary
if (widget.matrixFile?.tokens != null) {
widget.overlayController?.highlightCurrentText(
state.inMilliseconds,
widget.matrixFile!.tokens!,
);
}
});
_onAudioStateChanged?.cancel();
_onAudioStateChanged =
matrix.audioPlayer!.playerStateStream.listen((state) {
@ -584,13 +566,25 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
: Colors.transparent,
max: maxPosition,
value: currentPosition,
onChanged: (position) => audioPlayer == null
? _onButtonTap()
: audioPlayer.seek(
Duration(
milliseconds: position.round(),
),
),
// #Pangea
onChanged: !widget.enableClicks
? null
: (position) => audioPlayer == null
? _onButtonTap()
: audioPlayer.seek(
Duration(
milliseconds:
position.round(),
),
),
// onChanged: (position) => audioPlayer == null
// ? _onButtonTap()
// : audioPlayer.seek(
// Duration(
// milliseconds: position.round(),
// ),
// ),
// Pangea#
),
),
],
@ -625,7 +619,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
child: InkWell(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
onTap: _toggleSpeed,
onTap: !widget.enableClicks ? null : _toggleSpeed,
child: SizedBox(
width: 32,
height: 20,

View file

@ -13,16 +13,17 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/message_token_text/token_emoji_button.dart';
import 'package:fluffychat/pangea/message_token_text/token_practice_button.dart';
import 'package:fluffychat/pangea/message_token_text/tokens_util.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/pangea/toolbar/layout/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/token_practice_button.dart';
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/token_emoji_button.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/token_rendering_util.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/tokens_util.dart';
import 'package:fluffychat/utils/event_checkbox_extension.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/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import '../../../utils/url_launcher.dart';
@ -158,8 +159,7 @@ class HtmlMessage extends StatelessWidget {
?.where(
(t) =>
!["SYM"].contains(t.pos) &&
!t.lemma.text.contains(RegExp(r'[0-9]')) &&
t.lemma.text.length <= 50,
!t.lemma.text.contains(RegExp(r'[0-9]')),
)
.toList();
@ -335,7 +335,13 @@ class HtmlMessage extends StatelessWidget {
// Pangea#
int depth = 1,
}) {
final onlyElements = nodes.whereType<dom.Element>().toList();
// #Pangea
// final onlyElements = nodes.whereType<dom.Element>().toList();
final onlyElements = nodes
.whereType<dom.Element>()
.where((e) => e.localName != 'nontoken')
.toList();
// Pangea#
return [
for (var i = 0; i < nodes.length; i++) ...[
// Actually render the node child:
@ -347,8 +353,10 @@ class HtmlMessage extends StatelessWidget {
if (nodes[i] is dom.Element &&
onlyElements.indexOf(nodes[i] as dom.Element) <
onlyElements.length - 1) ...[
if (blockHtmlTags.contains((nodes[i] as dom.Element).localName))
const TextSpan(text: '\n\n'),
// #Pangea
// if (blockHtmlTags.contains((nodes[i] as dom.Element).localName))
// const TextSpan(text: '\n\n'),
// Pangea#
if (fullLineHtmlTag.contains((nodes[i] as dom.Element).localName))
const TextSpan(text: '\n'),
],
@ -388,8 +396,6 @@ class HtmlMessage extends StatelessWidget {
// #Pangea
final renderer = TokenRenderingUtil(
pangeaMessageEvent: pangeaMessageEvent,
readingAssistanceMode: readingAssistanceMode,
existingStyle: pangeaMessageEvent != null
? textStyle.merge(
AppConfig.messageTextStyle(
@ -398,14 +404,21 @@ class HtmlMessage extends StatelessWidget {
),
)
: textStyle,
overlayController: overlayController,
isTransitionAnimation: isTransitionAnimation,
);
final fontSize = renderer.fontSize(context) ?? this.fontSize;
double fontSize = this.fontSize;
if (readingAssistanceMode == ReadingAssistanceMode.practiceMode) {
fontSize = (overlayController != null && overlayController!.maxWidth > 600
? Theme.of(context).textTheme.titleLarge?.fontSize
: Theme.of(context).textTheme.bodyLarge?.fontSize) ??
this.fontSize;
}
final underlineColor = Theme.of(context).colorScheme.primary.withAlpha(200);
final newTokens =
pangeaMessageEvent != null && !pangeaMessageEvent!.ownMessage
? TokensUtil.getNewTokens(pangeaMessageEvent!)
? TokensUtil.getNewTokensByEvent(pangeaMessageEvent!)
: [];
// Pangea#
@ -428,8 +441,9 @@ class HtmlMessage extends StatelessWidget {
final isNew = token != null && newTokens.contains(token.text);
final tokenWidth = renderer.tokenTextWidthForContainer(
context,
node.text,
Theme.of(context).colorScheme.primary.withAlpha(200),
fontSize: fontSize,
);
return TextSpan(
@ -441,32 +455,28 @@ class HtmlMessage extends StatelessWidget {
: PlaceholderAlignment.middle,
child: Column(
children: [
if (token != null &&
overlayController?.selectedMode == SelectMode.emoji)
if (token != null && overlayController != null)
TokenEmojiButton(
token: token,
eventId: event.eventId,
enabled: token.lemma.saveVocab,
targetId: overlayController!.tokenEmojiPopupKey(token),
onSelect: () =>
overlayController!.showTokenEmojiPopup(token),
selectModeNotifier: overlayController!.selectedMode,
onTap: () =>
overlayController!.onClickOverlayMessageToken(token),
textColor: textColor,
),
if (renderer.showCenterStyling && token != null)
if (readingAssistanceMode ==
ReadingAssistanceMode.practiceMode &&
token != null &&
overlayController != null)
TokenPracticeButton(
token: token,
overlayController: overlayController,
controller: overlayController!.practiceController,
textStyle: renderer.style(
context,
color: renderer.backgroundColor(
context,
selected,
highlighted,
isNew,
readingAssistanceMode ==
ReadingAssistanceMode.practiceMode,
),
fontSize: fontSize,
underlineColor: underlineColor,
),
width: tokenWidth,
animateIn: isTransitionAnimation,
textColor: textColor,
),
CompositedTransformTarget(
@ -484,33 +494,58 @@ class HtmlMessage extends StatelessWidget {
onTap: onClick != null && token != null
? () => onClick?.call(token)
: null,
child: RichText(
textDirection: pangeaMessageEvent?.textDirection,
text: TextSpan(
children: [
LinkifySpan(
text: node.text.trim(),
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
selected,
highlighted,
isNew,
readingAssistanceMode ==
ReadingAssistanceMode.practiceMode,
child: HoverBuilder(
builder: (context, hovered) {
return RichText(
textDirection: pangeaMessageEvent?.textDirection,
text: TextSpan(
children: [
LinkifySpan(
text: node.text.trim(),
style: renderer.style(
fontSize: fontSize,
underlineColor: underlineColor,
selected: selected,
highlighted: highlighted,
isNew: isNew,
practiceMode: readingAssistanceMode ==
ReadingAssistanceMode.practiceMode,
hovered: hovered,
),
linkStyle: linkStyle,
onOpen: (url) =>
UrlLauncher(context, url.url)
.launchUrl(),
),
),
linkStyle: linkStyle,
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
],
),
],
),
);
},
),
),
),
),
if (readingAssistanceMode ==
ReadingAssistanceMode.practiceMode &&
token != null &&
overlayController != null)
ListenableBuilder(
listenable: overlayController!.practiceController,
builder: (context, _) => AnimatedSize(
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
),
curve: Curves.easeOut,
child: SizedBox(
height: overlayController!
.practiceController.practiceMode !=
MessagePracticeMode.noneSelected
? 4.0
: 0.0,
width: tokenWidth,
),
),
),
],
// ),
),
@ -635,14 +670,8 @@ class HtmlMessage extends StatelessWidget {
TextSpan(
text: '',
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
false,
false,
false,
false,
),
underlineColor: underlineColor,
fontSize: fontSize,
),
),
// Pangea#
@ -653,14 +682,8 @@ class HtmlMessage extends StatelessWidget {
// #Pangea
// style: textStyle,
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
false,
false,
false,
false,
),
underlineColor: underlineColor,
fontSize: fontSize,
),
// Pangea#
),
@ -942,11 +965,13 @@ class HtmlMessage extends StatelessWidget {
: PlaceholderAlignment.middle,
child: Column(
children: [
if (node.localName == 'nontoken' &&
overlayController?.selectedMode == SelectMode.emoji)
if (node.localName == 'nontoken' && overlayController != null)
// Use TokenEmojiButton to ensure consistent vertical alignment for non-token elements (e.g., emojis) in practice mode.
TokenEmojiButton(
token: null,
eventId: event.eventId,
selectModeNotifier: overlayController!.selectedMode,
onTap: () {},
enabled: false,
textColor: textColor,
),
RichText(
text: TextSpan(
@ -959,6 +984,24 @@ class HtmlMessage extends StatelessWidget {
),
),
),
if (overlayController != null)
ListenableBuilder(
listenable: overlayController!.practiceController,
builder: (context, _) => AnimatedSize(
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
),
curve: Curves.easeOut,
child: SizedBox(
height:
overlayController!.practiceController.practiceMode !=
MessagePracticeMode.noneSelected
? 4.0
: 0.0,
width: 0,
),
),
),
],
),
);
@ -1021,35 +1064,33 @@ class HtmlMessage extends StatelessWidget {
// overflow: TextOverflow.fade,
// );
final parsed = parser.parse(_addTokenTags()).body ?? dom.Element.html('');
return SelectionArea(
child: GestureDetector(
onTap: () {
if (overlayController == null) {
controller.showToolbar(
pangeaMessageEvent?.event ?? event,
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
);
}
},
child: Text.rich(
textScaler: TextScaler.noScaling,
_renderHtml(
parsed,
context,
TextStyle(
fontSize: fontSize,
color: textColor,
),
),
style: TextStyle(
return GestureDetector(
onTap: () {
if (overlayController == null) {
controller.showToolbar(
pangeaMessageEvent?.event ?? event,
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
);
}
},
child: Text.rich(
textScaler: TextScaler.noScaling,
_renderHtml(
parsed,
context,
TextStyle(
fontSize: fontSize,
color: textColor,
),
maxLines: limitHeight ? 64 : null,
overflow: TextOverflow.fade,
),
style: TextStyle(
fontSize: fontSize,
color: textColor,
),
maxLines: limitHeight ? 64 : null,
overflow: TextOverflow.fade,
),
);
}

View file

@ -13,8 +13,12 @@ import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_roles_event_widget.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_settings_language_icon.dart';
import 'package:fluffychat/pangea/chat/extensions/custom_room_display_extension.dart';
import 'package:fluffychat/pangea/chat/widgets/request_regeneration_button.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
@ -52,6 +56,7 @@ class Message extends StatelessWidget {
// #Pangea
final ChatController controller;
final bool isButton;
final bool canRefresh;
// Pangea#
const Message(
@ -78,6 +83,7 @@ class Message extends StatelessWidget {
// #Pangea
required this.controller,
this.isButton = false,
this.canRefresh = false,
// Pangea#
super.key,
});
@ -137,14 +143,25 @@ class Message extends StatelessWidget {
// #Pangea
if (event.type == PangeaEventTypes.activityPlan &&
event.room.activityPlan != null) {
return ActivitySummary(
activity: event.room.activityPlan!,
room: event.room,
showInstructions: controller.showInstructions,
toggleInstructions: controller.toggleShowInstructions,
getParticipantOpacity: (role) =>
role == null || role.isFinished ? 0.5 : 1.0,
isParticipantSelected: (id) => controller.room.ownRoleState?.id == id,
return ValueListenableBuilder(
valueListenable: controller.activityController.showInstructions,
builder: (context, show, __) {
return ActivitySummary(
activity: event.room.activityPlan!,
room: event.room,
assignedRoles: event.room.hasArchivedActivity
? event.room.activityRoles?.roles ?? {}
: event.room.assignedRoles ?? {},
showInstructions: show,
toggleInstructions:
controller.activityController.toggleShowInstructions,
getParticipantOpacity: (role) =>
role == null || role.isFinished ? 0.5 : 1.0,
isParticipantSelected: (id) =>
controller.room.ownRoleState?.id == id,
usedVocab: controller.activityController.usedVocab,
);
},
);
}
@ -165,7 +182,7 @@ class Message extends StatelessWidget {
final ownMessage = event.senderId == client.userID;
final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft;
var color = theme.colorScheme.surfaceContainerHigh;
var color = theme.colorScheme.surfaceContainerHighest;
final displayTime = event.type == EventTypes.RoomCreate ||
nextEvent == null ||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
@ -453,11 +470,26 @@ class Message extends StatelessWidget {
context: context,
user: user,
onMention: onMention,
// #Pangea
room: controller.room,
// Pangea#
),
presenceUserId: user.stateKey,
presenceBackgroundColor: wallpaperMode
? Colors.transparent
: null,
// #Pangea
miniIcon:
user.id == BotName.byEnvironment
? BotSettingsLanguageIcon(
room: controller.room,
)
: null,
presenceOffset:
user.id == BotName.byEnvironment
? const Offset(0, 0)
: null,
// Pangea#
);
},
),
@ -564,236 +596,277 @@ class Message extends StatelessWidget {
FluffyThemes.animationCurve,
child:
// #Pangea
PressableButton(
triggerAnimation: controller
.showToolbarStream.stream
.where(
(eventID) =>
eventID == event.eventId,
),
depressed: !isButton,
borderRadius: borderRadius,
onPressed: () {
showToolbar(
pangeaMessageEvent,
);
},
color: color,
visible: isButton && !noBubble,
child:
// Pangea#
Container(
decoration: BoxDecoration(
color: noBubble
? Colors.transparent
: color,
borderRadius: borderRadius,
),
clipBehavior: Clip.antiAlias,
// #Pangea
child:
CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
event.eventId,
)
.link,
// child: BubbleBackground(
// colors: colors,
// ignore: noBubble || !ownMessage,
// scrollController: scrollController,
// Pangea#
child: Container(
// #Pangea
key: MatrixState.pAnyState
.layerLinkAndKey(
event.eventId,
)
.key,
SelectionContainer.disabled(
child: MouseRegion(
cursor:
SystemMouseCursors.click,
child: ValueListenableBuilder(
valueListenable: controller
.depressMessageButton,
// #Pangea
child: ShimmerBackground(
enabled: controller
.showMessageShimmer(
event,
),
// Pangea#
decoration: BoxDecoration(
borderRadius:
BorderRadius
.circular(
AppConfig
.borderRadius,
child: Container(
decoration:
BoxDecoration(
color: noBubble
? Colors
.transparent
: color,
borderRadius:
borderRadius,
),
clipBehavior:
Clip.antiAlias,
// #Pangea
child:
CompositedTransformTarget(
link: MatrixState
.pAnyState
.layerLinkAndKey(
event.eventId,
)
.link,
// child: BubbleBackground(
// colors: colors,
// ignore: noBubble || !ownMessage,
// scrollController: scrollController,
// Pangea#
child: Container(
// #Pangea
key: MatrixState
.pAnyState
.layerLinkAndKey(
event.eventId,
)
.key,
// Pangea#
decoration:
BoxDecoration(
borderRadius:
BorderRadius
.circular(
AppConfig
.borderRadius,
),
),
constraints:
const BoxConstraints(
maxWidth: FluffyThemes
.columnWidth *
1.5,
),
child: Column(
mainAxisSize:
MainAxisSize
.min,
crossAxisAlignment:
CrossAxisAlignment
.start,
children: <Widget>[
if ({
RelationshipTypes
.reply,
RelationshipTypes
.thread,
}.contains(
event
.relationshipType,
))
FutureBuilder<
Event?>(
future: event
.getReplyEvent(
timeline,
),
builder: (
BuildContext
context,
snapshot,
) {
final replyEvent = snapshot
.hasData
? snapshot
.data!
: Event(
eventId: event.relationshipEventId!,
content: {
'msgtype': 'm.text',
'body': '...',
},
// #Pangea
// senderId: event
// .senderId,
senderId: "",
// Pangea#
type: 'm.room.message',
room: event.room,
status: EventStatus.sent,
originServerTs: DateTime.now(),
);
return Padding(
padding:
const EdgeInsets.only(
left:
16,
right:
16,
top:
8,
),
child:
Material(
color:
Colors.transparent,
borderRadius:
ReplyContent.borderRadius,
child:
InkWell(
borderRadius:
ReplyContent.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent.eventId,
),
child:
AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage: ownMessage,
timeline: timeline,
),
),
),
),
);
},
),
MessageContent(
displayEvent,
textColor:
textColor,
linkColor:
linkColor,
onInfoTab:
onInfoTab,
borderRadius:
borderRadius,
timeline:
timeline,
selected:
selected,
// #Pangea
pangeaMessageEvent:
pangeaMessageEvent,
controller:
controller,
nextEvent:
nextEvent,
prevEvent:
previousEvent,
// Pangea#
),
if (event
.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
))
Padding(
padding:
const EdgeInsets
.only(
bottom:
8.0,
left:
16.0,
right:
16.0,
),
child: Row(
mainAxisSize:
MainAxisSize
.min,
spacing:
4.0,
children: [
Icon(
Icons
.edit_outlined,
color:
textColor.withAlpha(
164,
),
size:
14,
),
Text(
displayEvent
.originServerTs
.localizedTimeShort(
context,
),
style:
TextStyle(
color:
textColor.withAlpha(
164,
),
fontSize:
11,
),
),
],
),
)
// #Pangea
else if (canRefresh)
RequestRegenerationButton(
textColor:
textColor,
onPressed: () =>
controller
.requestRegeneration(
event
.eventId,
),
),
// Pangea#
],
),
),
),
),
constraints:
const BoxConstraints(
maxWidth: FluffyThemes
.columnWidth *
1.5,
),
child: Column(
mainAxisSize:
MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment
.start,
children: <Widget>[
if ({
RelationshipTypes
.reply,
RelationshipTypes
.thread,
}.contains(
event
.relationshipType,
))
FutureBuilder<
Event?>(
future: event
.getReplyEvent(
timeline,
),
builder: (
BuildContext
context,
snapshot,
) {
final replyEvent =
snapshot
.hasData
? snapshot
.data!
: Event(
eventId:
event.relationshipEventId!,
content: {
'msgtype': 'm.text',
'body': '...',
},
// #Pangea
// senderId: event
// .senderId,
senderId:
"",
// Pangea#
type:
'm.room.message',
room:
event.room,
status:
EventStatus.sent,
originServerTs:
DateTime.now(),
);
return Padding(
padding:
const EdgeInsets
.only(
left: 16,
right: 16,
top: 8,
),
child:
Material(
color: Colors
.transparent,
borderRadius:
ReplyContent
.borderRadius,
child:
InkWell(
borderRadius:
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent
.eventId,
),
child:
AbsorbPointer(
child:
ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
),
),
),
),
);
},
),
MessageContent(
displayEvent,
textColor:
textColor,
linkColor:
linkColor,
onInfoTab:
onInfoTab,
borderRadius:
borderRadius,
timeline: timeline,
selected: selected,
// #Pangea
pangeaMessageEvent:
pangeaMessageEvent,
controller:
controller,
nextEvent:
nextEvent,
prevEvent:
previousEvent,
// Pangea#
),
if (event
.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
))
Padding(
padding:
const EdgeInsets
.only(
bottom: 8.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisSize:
MainAxisSize
.min,
spacing: 4.0,
children: [
Icon(
Icons
.edit_outlined,
color: textColor
.withAlpha(
164,
),
size: 14,
),
Text(
displayEvent
.originServerTs
.localizedTimeShort(
context,
),
style:
TextStyle(
color: textColor
.withAlpha(
164,
),
fontSize:
11,
),
),
],
),
),
],
),
),
// #Pangea
builder: (
context,
depressed,
child,
) =>
PressableButton(
buttonHeight: 5,
depressed: !isButton ||
depressed,
borderRadius:
borderRadius,
onPressed: () {
showToolbar(
pangeaMessageEvent,
);
},
color: color,
visible:
isButton && !noBubble,
builder:
(context, _, __) =>
child!,
),
// Pangea#
),
),
),

View file

@ -10,9 +10,8 @@ import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/layout/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
import 'package:fluffychat/utils/event_checkbox_extension.dart';
import '../../../config/app_config.dart';
import '../../../utils/platform_infos.dart';
@ -135,16 +134,6 @@ class MessageContent extends StatelessWidget {
if (overlayController != null) {
overlayController?.onClickOverlayMessageToken(token);
return;
} else {
Future.delayed(
const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
), () {
TtsController.tryToSpeak(
token.text.content,
langCode: pangeaMessageEvent!.messageDisplayLangCode,
);
});
}
controller.showToolbar(
@ -214,12 +203,12 @@ class MessageContent extends StatelessWidget {
linkColor: linkColor,
fontSize: fontSize,
// #Pangea
chatController: controller,
eventId:
"${event.eventId}${overlayController != null ? '_overlay' : ''}",
roomId: event.room.id,
senderId: event.senderId,
autoplay: overlayController != null && isTransitionAnimation,
enableClicks: overlayController != null,
// Pangea#
);
}

View file

@ -1 +0,0 @@

View file

@ -10,9 +10,10 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/emoji_burst.dart';
import 'package:fluffychat/pages/chat/events/reaction_listener.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
@ -20,11 +21,13 @@ class PangeaMessageReactions extends StatefulWidget {
final Event event;
final Timeline timeline;
final ChatController controller;
final double? width;
const PangeaMessageReactions(
this.event,
this.timeline,
this.controller, {
this.width,
super.key,
});
@ -33,10 +36,10 @@ class PangeaMessageReactions extends StatefulWidget {
}
class _PangeaMessageReactionsState extends State<PangeaMessageReactions> {
StreamSubscription? _reactionSubscription;
Map<String, _ReactionEntry> _reactionMap = {};
Set<String> _newlyAddedReactions = {};
late Client client;
ReactionListener? _reactionListener;
@override
void initState() {
@ -46,23 +49,17 @@ class _PangeaMessageReactionsState extends State<PangeaMessageReactions> {
_setupReactionStream();
}
void _setupReactionStream() {
_reactionSubscription = widget.controller.room.client.onSync.stream.where(
(update) {
final room = widget.controller.room;
final timelineEvents = update.rooms?.join?[room.id]?.timeline?.events;
if (timelineEvents == null) return false;
@override
void dispose() {
_reactionListener?.dispose();
super.dispose();
}
final eventID = widget.event.eventId;
return timelineEvents.any(
(e) =>
e.type == EventTypes.Redaction ||
(e.type == EventTypes.Reaction &&
Event.fromMatrixEvent(e, room).relationshipEventId ==
eventID),
);
},
).listen(_onReactionUpdate);
void _setupReactionStream() {
_reactionListener = ReactionListener(
event: widget.event,
onUpdate: _onReactionUpdate,
);
}
void _onReactionUpdate(SyncUpdate update) {
@ -104,12 +101,6 @@ class _PangeaMessageReactionsState extends State<PangeaMessageReactions> {
_reactionMap = newReactionMap;
}
@override
void dispose() {
_reactionSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final reactionList = _reactionMap.values.toList()
@ -120,13 +111,12 @@ class _PangeaMessageReactionsState extends State<PangeaMessageReactions> {
.aggregatedEvents(widget.timeline, RelationshipTypes.reaction)
.toList();
return Directionality(
textDirection: ownMessage ? TextDirection.rtl : TextDirection.ltr,
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: ownMessage ? Alignment.bottomRight : Alignment.bottomLeft,
clipBehavior: Clip.none,
return SizedBox(
width: allReactionEvents.any((e) => e.status.isSending)
? null
: widget.width,
child: Directionality(
textDirection: ownMessage ? TextDirection.rtl : TextDirection.ltr,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runSpacing: 4.0,
@ -171,10 +161,16 @@ class _PangeaMessageReactionsState extends State<PangeaMessageReactions> {
e.senderId == e.room.client.userID &&
e.content.tryGetMap('m.relates_to')?['key'] == reaction.key,
);
if (evt != null) {
await showFutureLoadingDialog(
context: context,
future: () => evt.redactEvent(),
try {
await evt?.redactEvent();
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'message': 'Failed to redact reaction event',
'event_id': evt?.eventId,
},
);
}
} else {

View file

@ -0,0 +1,34 @@
import 'dart:async';
import 'package:matrix/matrix.dart';
class ReactionListener {
final Event event;
final Function(SyncUpdate) onUpdate;
StreamSubscription? _reactionSub;
ReactionListener({required this.event, required this.onUpdate}) {
_reactionSub = event.room.client.onSync.stream.where(
(update) {
final room = event.room;
final timelineEvents = update.rooms?.join?[room.id]?.timeline?.events;
if (timelineEvents == null) return false;
final eventID = event.eventId;
return timelineEvents.any(
(e) =>
e.type == EventTypes.Redaction ||
(e.type == EventTypes.Reaction &&
Event.fromMatrixEvent(e, room).relationshipEventId ==
eventID),
);
},
).listen(onUpdate);
}
void dispose() {
_reactionSub?.cancel();
_reactionSub = null;
}
}

View file

@ -1,15 +1,22 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:emojis/emoji.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:matrix/matrix.dart';
import 'package:slugify/slugify.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/text_editing/edit_type_enum.dart';
import 'package:fluffychat/pangea/choreographer/text_editing/pangea_text_controller.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/common/widgets/shrinkable_text.dart';
import 'package:fluffychat/pangea/learning_settings/tool_settings_enum.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/subscription/widgets/paywall_card.dart';
import 'package:fluffychat/utils/markdown_context_builder.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import '../../widgets/avatar.dart';
@ -28,9 +35,10 @@ class InputBar extends StatelessWidget {
// #Pangea
// final TextEditingController? controller;
final PangeaTextController? controller;
final String hintText;
final Choreographer choreographer;
final VoidCallback showNextMatch;
// Pangea#
final InputDecoration? decoration;
final InputDecoration decoration;
final ValueChanged<String>? onChanged;
final bool? autofocus;
final bool readOnly;
@ -44,25 +52,24 @@ class InputBar extends StatelessWidget {
this.onSubmitImage,
this.focusNode,
this.controller,
this.decoration,
required this.decoration,
this.onChanged,
this.autofocus,
this.textInputAction,
this.readOnly = false,
// #Pangea
required this.hintText,
required this.choreographer,
required this.showNextMatch,
// Pangea#
super.key,
});
List<Map<String, String?>> getSuggestions(String text) {
if (controller!.selection.baseOffset !=
controller!.selection.extentOffset ||
controller!.selection.baseOffset < 0) {
List<Map<String, String?>> getSuggestions(TextEditingValue text) {
if (text.selection.baseOffset != text.selection.extentOffset ||
text.selection.baseOffset < 0) {
return []; // no entries if there is selected text
}
final searchText =
controller!.text.substring(0, controller!.selection.baseOffset);
final searchText = text.text.substring(0, text.selection.baseOffset);
final ret = <Map<String, String?>>[];
const maxResults = 30;
@ -229,36 +236,28 @@ class InputBar extends StatelessWidget {
Widget buildSuggestion(
BuildContext context,
Map<String, String?> suggestion,
void Function(Map<String, String?>) onSelected,
Client? client,
) {
final theme = Theme.of(context);
const size = 30.0;
// #Pangea
// const padding = EdgeInsets.all(4.0);
const padding = EdgeInsets.all(8.0);
// Pangea#
if (suggestion['type'] == 'command') {
final command = suggestion['name']!;
final hint = commandHint(L10n.of(context), command);
return Tooltip(
message: hint,
waitDuration: const Duration(days: 1), // don't show on hover
child: Container(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
commandExample(command),
style: const TextStyle(fontFamily: 'RobotoMono'),
),
Text(
hint,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
],
child: ListTile(
onTap: () => onSelected(suggestion),
title: Text(
commandExample(command),
style: const TextStyle(fontFamily: 'RobotoMono'),
),
subtitle: Text(
hint,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
),
);
@ -268,29 +267,28 @@ class InputBar extends StatelessWidget {
return Tooltip(
message: label,
waitDuration: const Duration(days: 1), // don't show on hover
child: Container(
padding: padding,
child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')),
child: ListTile(
onTap: () => onSelected(suggestion),
title: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')),
),
);
}
if (suggestion['type'] == 'emote') {
return Container(
padding: padding,
child: Row(
return ListTile(
onTap: () => onSelected(suggestion),
leading: MxcImage(
// ensure proper ordering ...
key: ValueKey(suggestion['name']),
uri: suggestion['mxc'] is String
? Uri.parse(suggestion['mxc'] ?? '')
: null,
width: size,
height: size,
isThumbnail: false,
),
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
MxcImage(
// ensure proper ordering ...
key: ValueKey(suggestion['name']),
uri: suggestion['mxc'] is String
? Uri.parse(suggestion['mxc'] ?? '')
: null,
width: size,
height: size,
isThumbnail: false,
),
const SizedBox(width: 6),
Text(suggestion['name']!),
Expanded(
child: Align(
@ -316,39 +314,22 @@ class InputBar extends StatelessWidget {
}
if (suggestion['type'] == 'user' || suggestion['type'] == 'room') {
final url = Uri.parse(suggestion['avatar_url'] ?? '');
return Container(
padding: padding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Avatar(
mxContent: url,
name: suggestion.tryGet<String>('displayname') ??
suggestion.tryGet<String>('mxid'),
size: size,
client: client,
// #Pangea
userId: suggestion.tryGet<String>('mxid'),
// Pangea#
),
const SizedBox(width: 6),
// #Pangea
// Text(suggestion['displayname'] ?? suggestion['mxid']!),
Flexible(
child: Text(
suggestion['displayname'] ?? suggestion['mxid']!,
overflow: TextOverflow.ellipsis,
),
),
// Pangea#
],
return ListTile(
onTap: () => onSelected(suggestion),
leading: Avatar(
mxContent: url,
name: suggestion.tryGet<String>('displayname') ??
suggestion.tryGet<String>('mxid'),
size: size,
client: client,
),
title: Text(suggestion['displayname'] ?? suggestion['mxid']!),
);
}
return const SizedBox.shrink();
}
void insertSuggestion(_, Map<String, String?> suggestion) {
String insertSuggestion(Map<String, String?> suggestion) {
final replaceText =
controller!.text.substring(0, controller!.selection.baseOffset);
var startText = '';
@ -412,165 +393,186 @@ class InputBar extends StatelessWidget {
(Match m) => '${m[1]}$insertText',
);
}
if (insertText.isNotEmpty && startText.isNotEmpty) {
controller!.text = startText + afterText;
controller!.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
return startText + afterText;
}
// #Pangea
SubscriptionStatus get _subscriptionStatus =>
MatrixState.pangeaController.subscriptionController.subscriptionStatus;
String _defaultHintText(BuildContext context) {
return MatrixState.pangeaController.userController.languagesSet
? L10n.of(context).writeAMessageLangCodes(
MatrixState.pangeaController.userController.userL1!.displayName,
MatrixState.pangeaController.userController.userL2!.displayName,
)
: L10n.of(context).writeAMessage;
}
void _onInputTap(BuildContext context) {
if (_shouldShowPaywall(context)) return;
final baseOffset = controller!.selection.baseOffset;
final adjustedOffset = _adjustOffsetForNormalization(baseOffset);
final match = choreographer.igcController.getMatchByOffset(adjustedOffset);
if (match == null) return;
if (match.updatedMatch.isITStart) {
choreographer.itController.openIT(controller!.text);
} else {
OverlayUtil.showIGCMatch(
match,
choreographer,
context,
showNextMatch,
);
// rebuild the text field to highlight the newly selected match
choreographer.textController.setSystemText(
choreographer.textController.text,
EditTypeEnum.other,
);
choreographer.textController.selection = TextSelection.collapsed(
offset: baseOffset,
);
}
}
bool _shouldShowPaywall(BuildContext context) {
if (_subscriptionStatus == SubscriptionStatus.shouldShowPaywall) {
PaywallCard.show(context, ChoreoConstants.inputTransformTargetKey);
return true;
}
return false;
}
int _adjustOffsetForNormalization(int baseOffset) {
int adjustedOffset = baseOffset;
final corrections = choreographer.igcController.recentAutomaticCorrections;
for (final correction in corrections) {
final match = correction.updatedMatch.match;
if (match.offset < adjustedOffset && match.length > 0) {
adjustedOffset += (match.length - 1);
}
}
return adjustedOffset;
}
// Pangea#
@override
Widget build(BuildContext context) {
// #Pangea
final enableAutocorrect = MatrixState
.pangeaController.userController.profile.toolSettings.enableAutocorrect;
// Pangea#
return TypeAheadField<Map<String, String?>>(
direction: VerticalDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
controller: controller,
final theme = Theme.of(context);
return Autocomplete<Map<String, String?>>(
focusNode: focusNode,
hideOnSelect: false,
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
// #Pangea
builder: (context, _, focusNode) {
final textField = TextField(
enableSuggestions: enableAutocorrect,
readOnly:
controller != null && (controller!.choreographer.isRunningIT),
autocorrect: enableAutocorrect,
controller: controller,
focusNode: focusNode,
contextMenuBuilder: (c, e) => markdownContextBuilder(
c,
e,
_,
),
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
final data = content.data;
if (data == null) return;
textEditingController: controller,
optionsBuilder: getSuggestions,
fieldViewBuilder: (context, __, focusNode, _) => ValueListenableBuilder(
valueListenable: choreographer.itController.open,
builder: (context, _, __) {
return TextField(
controller: controller,
focusNode: focusNode,
// #Pangea
// readOnly: readOnly,
// contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
contextMenuBuilder: (c, e) =>
markdownContextBuilder(c, e, controller!),
onTap: () => _onInputTap(context),
readOnly: choreographer.choreoMode == ChoreoModeEnum.it,
autocorrect: MatrixState.pangeaController.userController
.isToolEnabled(ToolSetting.enableAutocorrect),
// Pangea#
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
final data = content.data;
if (data == null) return;
final file = MatrixFile(
mimeType: content.mimeType,
bytes: data,
name: content.uri.split('/').last,
);
room.sendFileEvent(
file,
shrinkImageMaxDimension: 1600,
);
final file = MatrixFile(
mimeType: content.mimeType,
bytes: data,
name: content.uri.split('/').last,
);
room.sendFileEvent(
file,
shrinkImageMaxDimension: 1600,
);
},
),
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType!,
textInputAction: textInputAction,
autofocus: autofocus!,
inputFormatters: [
// #Pangea
//LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
//setting max character count to 1000
//after max, nothing else can be typed
LengthLimitingTextInputFormatter(1000),
// Pangea#
],
onSubmitted: (text) {
// fix for library for now
// it sets the types for the callback incorrectly
onSubmitted!(text);
},
),
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType!,
textInputAction: textInputAction,
autofocus: autofocus!,
inputFormatters: [
//LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
//setting max character count to 1000
//after max, nothing else can be typed
LengthLimitingTextInputFormatter(1000),
],
onSubmitted: (text) {
// fix for library for now
// it sets the types for the callback incorrectly
onSubmitted!(text);
},
style: controller?.exceededMaxLength ?? false
? const TextStyle(color: Colors.red)
: null,
onTap: () {
controller?.onInputTap(
context,
fNode: focusNode,
);
},
decoration: decoration!,
onChanged: (text) {
// fix for the library for now
// it sets the types for the callback incorrectly
onChanged!(text);
},
textCapitalization: TextCapitalization.sentences,
);
// fix for issue with typing not working sometimes on Firefox and Safari
return Stack(
alignment: Alignment.centerLeft,
children: [
if (controller != null && controller!.text.isEmpty)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ShrinkableText(
text: hintText,
maxWidth: double.infinity,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).disabledColor,
),
// #Pangea
// maxLength: AppSettings.textMessageMaxLength.value,
// decoration: decoration!,
// Pangea#
decoration: decoration.copyWith(
// #Pangea
// hint: ShrinkableText(
hint: StreamBuilder(
stream: MatrixState
.pangeaController.userController.languageStream.stream,
builder: (context, _) => SizedBox(
height: 24,
child: ShrinkableText(
// Pangea#
text: choreographer.itController.open.value
? L10n.of(context).buildTranslation
: _defaultHintText(context),
maxWidth: double.infinity,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).disabledColor,
),
),
),
),
kIsWeb ? SelectionArea(child: textField) : textField,
],
),
onChanged: (text) {
// fix for the library for now
// it sets the types for the callback incorrectly
onChanged!(text);
},
textCapitalization: TextCapitalization.sentences,
);
},
),
optionsViewBuilder: (c, onSelected, s) {
final suggestions = s.toList();
return Material(
elevation: theme.appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: theme.appBarTheme.shadowColor,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
child: ListView.builder(
shrinkWrap: true,
itemCount: suggestions.length,
itemBuilder: (context, i) => buildSuggestion(
c,
suggestions[i],
onSelected,
Matrix.of(context).client,
),
),
);
},
// builder: (context, controller, focusNode) => TextField(
// controller: controller,
// focusNode: focusNode,
// readOnly: readOnly,
// contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
// contentInsertionConfiguration: ContentInsertionConfiguration(
// onContentInserted: (KeyboardInsertedContent content) {
// final data = content.data;
// if (data == null) return;
// final file = MatrixFile(
// mimeType: content.mimeType,
// bytes: data,
// name: content.uri.split('/').last,
// );
// room.sendFileEvent(
// file,
// shrinkImageMaxDimension: 1600,
// );
// },
// ),
// minLines: minLines,
// maxLines: maxLines,
// keyboardType: keyboardType!,
// textInputAction: textInputAction,
// autofocus: autofocus!,
// inputFormatters: [
// LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
// ],
// onSubmitted: (text) {
// // fix for library for now
// // it sets the types for the callback incorrectly
// onSubmitted!(text);
// },
// decoration: decoration!,
// onChanged: (text) {
// // fix for the library for now
// // it sets the types for the callback incorrectly
// onChanged!(text);
// },
// textCapitalization: TextCapitalization.sentences,
// ),
// Pangea#
suggestionsCallback: getSuggestions,
itemBuilder: (c, s) => buildSuggestion(c, s, Matrix.of(context).client),
onSelected: (Map<String, String?> suggestion) =>
insertSuggestion(context, suggestion),
errorBuilder: (BuildContext context, Object? error) =>
const SizedBox.shrink(),
loadingBuilder: (BuildContext context) => const SizedBox.shrink(),
// fix loading briefly flickering a dark box
emptyBuilder: (BuildContext context) =>
const SizedBox.shrink(), // fix loading briefly showing no suggestions
displayStringForOption: insertSuggestion,
optionsViewOpenDirection: OptionsViewOpenDirection.up,
);
}
}

View file

@ -12,11 +12,13 @@ import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/toolbar/utils/update_version_dialog.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'events/audio_player.dart';
class PermissionException implements Exception {}
class RecordingDialog extends StatefulWidget {
const RecordingDialog({
super.key,
@ -30,7 +32,10 @@ class RecordingDialogState extends State<RecordingDialog> {
Timer? _recorderSubscription;
Duration _duration = Duration.zero;
bool error = false;
// #Pangea
// bool error = false;
Object? error;
// Pangea#
final _audioRecorder = AudioRecorder();
final List<double> amplitudeTimeline = [];
@ -61,37 +66,27 @@ class RecordingDialogState extends State<RecordingDialog> {
final result = await _audioRecorder.hasPermission();
if (result != true) {
setState(() => error = true);
return;
// #Pangea
throw PermissionException();
// setState(() => error = true);
// return;
// Pangea#
}
await WakelockPlus.enable();
// #Pangea
final isNotError = await showUpdateVersionDialog(
future: () async =>
// Pangea#
await _audioRecorder.start(
RecordConfig(
bitRate: AppSettings.audioRecordingBitRate.getItem(store),
sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store),
numChannels: AppSettings.audioRecordingNumChannels.getItem(store),
autoGain: AppSettings.audioRecordingAutoGain.getItem(store),
echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store),
noiseSuppress:
AppSettings.audioRecordingNoiseSuppress.getItem(store),
encoder: codec,
),
path: path ?? '',
await _audioRecorder.start(
RecordConfig(
bitRate: AppSettings.audioRecordingBitRate.getItem(store),
sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store),
numChannels: AppSettings.audioRecordingNumChannels.getItem(store),
autoGain: AppSettings.audioRecordingAutoGain.getItem(store),
echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store),
noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store),
encoder: codec,
),
// #Pangea
context: context,
path: path ?? '',
);
if (!isNotError) {
Navigator.of(context).pop();
return;
}
// Pangea#
setState(() => _duration = Duration.zero);
_recorderSubscription?.cancel();
_recorderSubscription =
@ -104,8 +99,12 @@ class RecordingDialogState extends State<RecordingDialog> {
_duration += const Duration(milliseconds: 100);
});
});
} catch (_) {
setState(() => error = true);
// #Pangea
// } catch (_) {
// setState(() => error = true);
} catch (e) {
setState(() => error = e);
// Pangea#
rethrow;
}
}
@ -154,8 +153,19 @@ class RecordingDialogState extends State<RecordingDialog> {
const maxDecibalWidth = 64.0;
final time =
'${_duration.inMinutes.toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}';
final content = error
? Text(L10n.of(context).oopsSomethingWentWrong)
// #Pangea
// final content = error
// ? Text(L10n.of(context).oopsSomethingWentWrong)
final content = error != null
? ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250.0),
child: error is PermissionException
? Text(L10n.of(context).recordingPermissionDenied)
: kIsWeb
? Text(L10n.of(context).genericWebRecordingError)
: Text(error!.toLocalizedString(context)),
)
// Pangea#
: Row(
children: [
Container(
@ -209,7 +219,10 @@ class RecordingDialogState extends State<RecordingDialog> {
),
),
),
if (error != true)
// #Pangea
// if (error != true)
if (error == null)
// Pangea#
CupertinoDialogAction(
onPressed: _stopAndSend,
child: Text(L10n.of(context).send),
@ -229,7 +242,10 @@ class RecordingDialogState extends State<RecordingDialog> {
),
),
),
if (error != true)
// #Pangea
// if (error != true)
if (error == null)
// Pangea#
TextButton(
onPressed: _stopAndSend,
child: Text(L10n.of(context).send),

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@ -7,6 +9,7 @@ import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat_details/chat_download_provider.dart';
import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/chat_settings/pages/pangea_room_details.dart';
@ -14,10 +17,9 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/download/download_room_extension.dart';
import 'package:fluffychat/pangea/download/download_type_enum.dart';
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -51,24 +53,47 @@ class ChatDetails extends StatefulWidget {
// #Pangea
// class ChatDetailsController extends State<ChatDetails> {
class ChatDetailsController extends State<ChatDetails>
with ActivitySummariesProvider, CoursePlanProvider {
with ActivitySummariesProvider, CoursePlanProvider, ChatDownloadProvider {
bool loadingActivities = true;
bool loadingCourseSummary = true;
// listen to language updates to refresh course info
StreamSubscription? _languageSubscription;
@override
void initState() {
super.initState();
_loadCourseInfo();
_loadSummaries();
_languageSubscription = MatrixState
.pangeaController.userController.languageStream.stream
.listen((update) {
if (update.prevBaseLang != update.baseLang) {
_loadCourseInfo();
}
});
}
@override
void didUpdateWidget(covariant ChatDetails oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.roomId != widget.roomId) {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
if (oldWidget.roomId != widget.roomId ||
course?.uuid != room?.coursePlan?.uuid) {
_loadCourseInfo();
_loadSummaries();
}
if (widget.activeTab == 'course' && oldWidget.activeTab != 'course') {
_loadSummaries();
}
}
@override
void dispose() {
_languageSubscription?.cancel();
super.dispose();
}
// Pangea#
@ -227,52 +252,6 @@ class ChatDetailsController extends State<ChatDetails>
}
// #Pangea
void downloadChatAction() async {
if (roomId == null) return;
final Room? room = Matrix.of(context).client.getRoomById(roomId!);
if (room == null) return;
final type = await showModalActionPopup(
context: context,
title: L10n.of(context).downloadGroupText,
actions: [
AdaptiveModalAction(
value: DownloadType.csv,
label: L10n.of(context).downloadCSVFile,
),
AdaptiveModalAction(
value: DownloadType.txt,
label: L10n.of(context).downloadTxtFile,
),
AdaptiveModalAction(
value: DownloadType.xlsx,
label: L10n.of(context).downloadXLSXFile,
),
],
);
if (type == null) return;
try {
await room.download(type, context);
} on EmptyChatException {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
L10n.of(context).emptyChatDownloadWarning,
),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"${L10n.of(context).oopsSomethingWentWrong} ${L10n.of(context).errorPleaseRefresh}",
),
),
);
}
}
Future<void> setRoomCapacity() async {
if (roomId == null) return;
final Room? room = Matrix.of(context).client.getRoomById(roomId!);
@ -381,7 +360,7 @@ class ChatDetailsController extends State<ChatDetails>
);
if (resp.isError || resp.result == null || !mounted) return;
context.go('/rooms/${resp.result}/invite');
NavigationUtil.goToSpaceRoute(resp.result, ['invite'], context);
}
Future<void> _loadCourseInfo() async {

View file

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/download/download_room_extension.dart';
import 'package:fluffychat/pangea/download/download_type_enum.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
import 'package:fluffychat/widgets/matrix.dart';
mixin ChatDownloadProvider {
void downloadChatAction(String roomId, BuildContext context) async {
final Room? room = Matrix.of(context).client.getRoomById(roomId);
if (room == null) return;
final type = await showModalActionPopup(
context: context,
title: L10n.of(context).downloadGroupText,
actions: [
AdaptiveModalAction(
value: DownloadType.csv,
label: L10n.of(context).downloadCSVFile,
),
AdaptiveModalAction(
value: DownloadType.txt,
label: L10n.of(context).downloadTxtFile,
),
AdaptiveModalAction(
value: DownloadType.xlsx,
label: L10n.of(context).downloadXLSXFile,
),
],
);
if (type == null) return;
try {
await room.download(type, context);
} on EmptyChatException {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
L10n.of(context).emptyChatDownloadWarning,
),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"${L10n.of(context).oopsSomethingWentWrong} ${L10n.of(context).errorPleaseRefresh}",
),
),
);
}
}
}

View file

@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:app_links/app_links.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter_shortcuts_new/flutter_shortcuts_new.dart';
import 'package:go_router/go_router.dart';
@ -21,6 +20,9 @@ import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart
import 'package:fluffychat/pangea/chat_settings/widgets/chat_context_menu_action.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
import 'package:fluffychat/pangea/join_codes/space_code_repo.dart';
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
@ -105,7 +107,9 @@ class ChatListController extends State<ChatList>
StreamSubscription? _intentFileStreamSubscription;
StreamSubscription? _intentUriStreamSubscription;
// #Pangea
// StreamSubscription? _intentUriStreamSubscription;
// Pangea#
ActiveFilter activeFilter = AppConfig.separateChatTypes
? ActiveFilter.messages
@ -247,7 +251,10 @@ class ChatListController extends State<ChatList>
return;
}
context.go('/rooms/${room.id}');
// #Pangea
// context.go('/rooms/${room.id}');
NavigationUtil.goToSpaceRoute(room.id, [], context);
// Pangea#
}
bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) {
@ -255,36 +262,24 @@ class ChatListController extends State<ChatList>
case ActiveFilter.allChats:
// #Pangea
// return (room) => true;
return (room) =>
!room.isHiddenRoom &&
!room.isSpace &&
room.firstSpaceParent == null;
return (room) => !room.isHiddenRoom && !room.isSpace;
// Pangea#
case ActiveFilter.messages:
// #Pangea
// return (room) => !room.isSpace && room.isDirectChat;
return (room) =>
!room.isSpace &&
room.isDirectChat &&
!room.isHiddenRoom &&
room.firstSpaceParent == null;
!room.isSpace && room.isDirectChat && !room.isHiddenRoom;
// Pangea#
case ActiveFilter.groups:
// #Pangea
// return (room) => !room.isSpace && !room.isDirectChat;
return (room) =>
!room.isSpace &&
!room.isDirectChat &&
!room.isHiddenRoom &&
room.firstSpaceParent == null;
!room.isSpace && !room.isDirectChat && !room.isHiddenRoom;
// Pangea#
case ActiveFilter.unread:
// #Pangea
// return (room) => room.isUnreadOrInvited;
return (room) =>
room.isUnreadOrInvited &&
!room.isHiddenRoom &&
room.firstSpaceParent == null;
return (room) => room.isUnreadOrInvited && !room.isHiddenRoom;
// Pangea#
case ActiveFilter.spaces:
return (room) => room.isSpace;
@ -463,6 +458,9 @@ class ChatListController extends State<ChatList>
void _processIncomingSharedMedia(List<SharedMediaFile> files) {
if (files.isEmpty) return;
// #Pangea
if (files.every((f) => f.type == SharedMediaType.url)) return;
// Pangea#
showScaffoldDialog(
context: context,
@ -487,13 +485,15 @@ class ChatListController extends State<ChatList>
);
}
void _processIncomingUris(Uri? uri) async {
if (uri == null) return;
context.go('/rooms');
WidgetsBinding.instance.addPostFrameCallback((_) {
UrlLauncher(context, uri.toString()).openMatrixToUrl();
});
}
// #Pangea
// void _processIncomingUris(Uri? uri) async {
// if (uri == null) return;
// context.go('/rooms');
// WidgetsBinding.instance.addPostFrameCallback((_) {
// UrlLauncher(context, uri.toString()).openMatrixToUrl();
// });
// }
// Pangea#
void _initReceiveSharingIntent() {
if (!PlatformInfos.isMobile) return;
@ -508,9 +508,11 @@ class ChatListController extends State<ChatList>
.getInitialMedia()
.then(_processIncomingSharedMedia);
// For receiving shared Uris
_intentUriStreamSubscription =
AppLinks().uriLinkStream.listen(_processIncomingUris);
// #Pangea
// // For receiving shared Uris
// _intentUriStreamSubscription =
// AppLinks().uriLinkStream.listen(_processIncomingUris);
// Pangea#
if (PlatformInfos.isAndroid) {
final shortcuts = FlutterShortcuts();
@ -525,7 +527,6 @@ class ChatListController extends State<ChatList>
//#Pangea
StreamSubscription? _invitedSpaceSubscription;
StreamSubscription? _subscriptionStatusStream;
StreamSubscription? _roomCapacitySubscription;
//Pangea#
@ -556,8 +557,10 @@ class ChatListController extends State<ChatList>
_checkTorBrowser();
//#Pangea
_invitedSpaceSubscription = MatrixState
.pangeaController.matrixState.client.onSync.stream
_invitedSpaceSubscription = Matrix.of(context)
.client
.onSync
.stream
.where((event) => event.rooms?.invite != null)
.listen((event) async {
for (final inviteEntry in event.rooms!.invite!.entries) {
@ -575,15 +578,12 @@ class ChatListController extends State<ChatList>
if (isSpace) {
final spaceId = inviteEntry.key;
final space =
MatrixState.pangeaController.matrixState.client.getRoomById(
spaceId,
);
final space = Matrix.of(context).client.getRoomById(
spaceId,
);
final String? justInputtedCode =
MatrixState.pangeaController.spaceCodeController.justInputtedCode;
final newSpaceCode = space?.classCode;
if (newSpaceCode?.toLowerCase() == justInputtedCode?.toLowerCase()) {
if (space?.classCode?.toLowerCase() ==
SpaceCodeRepo.recentCode?.toLowerCase()) {
return;
}
@ -596,8 +596,8 @@ class ChatListController extends State<ChatList>
}
if (isAnalytics) {
final analyticsRoom = MatrixState.pangeaController.matrixState.client
.getRoomById(inviteEntry.key);
final analyticsRoom =
Matrix.of(context).client.getRoomById(inviteEntry.key);
try {
await analyticsRoom?.join();
} catch (err, s) {
@ -613,18 +613,13 @@ class ChatListController extends State<ChatList>
}
});
_subscriptionStatusStream ??= MatrixState
.pangeaController.subscriptionController.subscriptionStream.stream
.listen((event) {
if (mounted) {
showSubscribedSnackbar(context);
}
});
MatrixState.pangeaController.subscriptionController.subscriptionNotifier
.addListener(_onSubscribe);
// listen for space child updates for any space that is not the active space
// so that when the user navigates to the space that was updated, it will
// reload any rooms that have been added / removed
final client = MatrixState.pangeaController.matrixState.client;
final client = Matrix.of(context).client;
// listen for room join events and leave room if over capacity
_roomCapacitySubscription ??= client.onSync.stream
@ -656,7 +651,7 @@ class ChatListController extends State<ChatList>
future: () async {
await room.leave();
if (GoRouterState.of(context).uri.toString().contains(roomID)) {
context.go("/rooms");
NavigationUtil.goToSpaceRoute(null, [], context);
}
throw L10n.of(context).roomFull;
},
@ -673,6 +668,10 @@ class ChatListController extends State<ChatList>
}
// #Pangea
void _onSubscribe() {
if (mounted) showSubscribedSnackbar(context);
}
Future<void> _joinInvitedSpaces() async {
final invitedSpaces = Matrix.of(context).client.rooms.where(
(r) => r.isSpace && r.membership == Membership.invite,
@ -688,11 +687,13 @@ class ChatListController extends State<ChatList>
void dispose() {
_intentDataStreamSubscription?.cancel();
_intentFileStreamSubscription?.cancel();
_intentUriStreamSubscription?.cancel();
//#Pangea
// _intentUriStreamSubscription?.cancel();
_invitedSpaceSubscription?.cancel();
_subscriptionStatusStream?.cancel();
_roomCapacitySubscription?.cancel();
MatrixState.pangeaController.subscriptionController.subscriptionNotifier
.removeListener(_onSubscribe);
SpaceCodeController.codeNotifier.removeListener(_onCacheSpaceCode);
//Pangea#
scrollController.removeListener(_onScroll);
super.dispose();
@ -1103,10 +1104,15 @@ class ChatListController extends State<ChatList>
void _initPangeaControllers(Client client) {
MatrixState.pangeaController.initControllers();
if (mounted) {
MatrixState.pangeaController.spaceCodeController
.joinCachedSpaceCode(context);
SpaceCodeController.joinCachedSpaceCode(context);
SpaceCodeController.codeNotifier.addListener(_onCacheSpaceCode);
}
}
void _onCacheSpaceCode() {
if (!mounted) return;
SpaceCodeController.joinCachedSpaceCode(context);
}
// Pangea#
void setActiveFilter(ActiveFilter filter) {

View file

@ -8,16 +8,13 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
import 'package:fluffychat/pangea/chat_list/widgets/pangea_chat_list_header.dart';
import 'package:fluffychat/pangea/chat_settings/utils/bot_client_extension.dart';
import 'package:fluffychat/pangea/course_chats/course_chats_page.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import '../../config/themes.dart';
@ -68,11 +65,11 @@ class ChatListViewBody extends StatelessWidget {
// final publicRooms = controller.roomSearchResult?.chunk
// .where((room) => room.roomType != 'm.space')
// .toList();
// final publicSpaces = controller.roomSearchResult?.chunk
// .where((room) => room.roomType == 'm.space')
// .toList();
// final userSearchResult = controller.userSearchResult;
// Pangea#
final publicSpaces = controller.roomSearchResult?.chunk
.where((room) => room.roomType == 'm.space')
.toList();
final userSearchResult = controller.userSearchResult;
const dummyChatCount = 4;
final filter = controller.searchController.text.toLowerCase();
return StreamBuilder(
@ -106,53 +103,44 @@ class ChatListViewBody extends StatelessWidget {
// icon: const Icon(Icons.explore_outlined),
// ),
// PublicRoomsHorizontalList(publicRooms: publicRooms),
// Pangea#
SearchTitle(
// #Pangea
// title: L10n.of(context).publicSpaces,
title: L10n.of(context).publicCourses,
// icon: const Icon(Icons.workspaces_outlined),
icon: const Icon(Icons.groups_outlined),
// Pangea#
),
PublicRoomsHorizontalList(publicRooms: publicSpaces),
SearchTitle(
title: L10n.of(context).users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: userSearchResult == null ||
userSearchResult.results.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: userSearchResult == null
? null
// #Pangea
: UserSearchResultsList(
userSearchResult: userSearchResult,
),
// : ListView.builder(
// scrollDirection: Axis.horizontal,
// itemCount: userSearchResult.results.length,
// itemBuilder: (context, i) => _SearchItem(
// title:
// userSearchResult.results[i].displayName ??
// userSearchResult
// .results[i].userId.localpart ??
// L10n.of(context).unknownDevice,
// avatar: userSearchResult.results[i].avatarUrl,
// onPressed: () => UserDialog.show(
// context: context,
// profile: userSearchResult.results[i],
// ),
// ),
// ),
// Pangea#
),
// SearchTitle(
// title: L10n.of(context).publicSpaces,
// icon: const Icon(Icons.workspaces_outlined),
// ),
// PublicRoomsHorizontalList(publicRooms: publicSpaces),
// SearchTitle(
// title: L10n.of(context).users,
// icon: const Icon(Icons.group_outlined),
// ),
// AnimatedContainer(
// clipBehavior: Clip.hardEdge,
// decoration: const BoxDecoration(),
// height: userSearchResult == null ||
// userSearchResult.results.isEmpty
// ? 0
// : 106,
// duration: FluffyThemes.animationDuration,
// curve: FluffyThemes.animationCurve,
// child: userSearchResult == null
// ? null
// : ListView.builder(
// scrollDirection: Axis.horizontal,
// itemCount: userSearchResult.results.length,
// itemBuilder: (context, i) => _SearchItem(
// title:
// userSearchResult.results[i].displayName ??
// userSearchResult
// .results[i].userId.localpart ??
// L10n.of(context).unknownDevice,
// avatar: userSearchResult.results[i].avatarUrl,
// onPressed: () => UserDialog.show(
// context: context,
// profile: userSearchResult.results[i],
// ),
// ),
// ),
// ),
// Pangea#
],
// #Pangea
// if (!controller.isSearchMode && AppConfig.showPresences)
@ -220,21 +208,29 @@ class ChatListViewBody extends StatelessWidget {
// .toList(),
// ),
// ),
// Pangea#
if (controller.isSearchMode)
SearchTitle(
title: L10n.of(context).chats,
icon: const Icon(Icons.forum_outlined),
// if (controller.isSearchMode)
// SearchTitle(
// title: L10n.of(context).chats,
// icon: const Icon(Icons.forum_outlined),
// ),
if (controller.isSearchMode &&
rooms
.where(
(room) => room
.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
)
.toLowerCase()
.contains(filter),
)
.isEmpty)
Padding(
padding: const EdgeInsetsGeometry.all(16.0),
child: Text(
L10n.of(context).emptyChatSearch,
textAlign: TextAlign.center,
),
),
// #Pangea
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.chatListTooltip,
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 16.0,
),
),
// if (client.prevBatch != null &&
// rooms.isEmpty &&
// !controller.isSearchMode) ...[
@ -356,10 +352,7 @@ class ChatListViewBody extends StatelessWidget {
}
}
// #Pangea
// class PublicRoomsHorizontalList extends StatelessWidget {
class PublicRoomsHorizontalList extends StatefulWidget {
// Pangea#
class PublicRoomsHorizontalList extends StatelessWidget {
const PublicRoomsHorizontalList({
super.key,
required this.publicRooms,
@ -367,23 +360,6 @@ class PublicRoomsHorizontalList extends StatefulWidget {
final List<PublicRoomsChunk>? publicRooms;
// #Pagngea
@override
PublicRoomsHorizontalListState createState() =>
PublicRoomsHorizontalListState();
}
class PublicRoomsHorizontalListState extends State<PublicRoomsHorizontalList> {
List<PublicRoomsChunk>? get publicRooms => widget.publicRooms;
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
// Pangea#
@override
Widget build(BuildContext context) {
final publicRooms = this.publicRooms;
@ -395,46 +371,21 @@ class PublicRoomsHorizontalListState extends State<PublicRoomsHorizontalList> {
curve: FluffyThemes.animationCurve,
child: publicRooms == null
? null
:
// #Pangea
Scrollbar(
thumbVisibility: true,
controller: _scrollController,
child:
// Pangea#
ListView.builder(
// #Pangea
controller: _scrollController,
// Pangea#
scrollDirection: Axis.horizontal,
itemCount: publicRooms.length,
itemBuilder: (context, i) => _SearchItem(
title: publicRooms[i].name ??
publicRooms[i].canonicalAlias?.localpart ??
// #Pangea
// L10n.of(context).group,
L10n.of(context).chat,
// Pangea#
avatar: publicRooms[i].avatarUrl,
// #Pangea
onPressed: () => PublicRoomBottomSheet.show(
context: context,
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: publicRooms.length,
itemBuilder: (context, i) => _SearchItem(
title: publicRooms[i].name ??
publicRooms[i].canonicalAlias?.localpart ??
L10n.of(context).group,
avatar: publicRooms[i].avatarUrl,
onPressed: () => showAdaptiveDialog(
context: context,
builder: (c) => PublicRoomDialog(
roomAlias:
publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,
chunk: publicRooms[i],
),
// onPressed: () => showAdaptiveDialog(
// context: context,
// builder: (c) => PublicRoomDialog(
// roomAlias: publicRooms[i].canonicalAlias ??
// publicRooms[i].roomId,
// chunk: publicRooms[i],
// ),
// ),
radius: BorderRadius.circular(
AppConfig.borderRadius / 2,
),
// Pangea#
),
),
),
@ -446,19 +397,11 @@ class _SearchItem extends StatelessWidget {
final String title;
final Uri? avatar;
final void Function() onPressed;
// #Pangea
final BorderRadius? radius;
final String? userId;
// Pangea#
const _SearchItem({
required this.title,
this.avatar,
required this.onPressed,
// #Pangea
this.radius,
this.userId,
// Pangea#
});
@override
@ -473,10 +416,6 @@ class _SearchItem extends StatelessWidget {
Avatar(
mxContent: avatar,
name: title,
// #Pangea
borderRadius: radius,
userId: userId,
// Pangea#
),
Padding(
padding: const EdgeInsets.all(8.0),
@ -495,50 +434,3 @@ class _SearchItem extends StatelessWidget {
),
);
}
// #Pangea
class UserSearchResultsList extends StatefulWidget {
final SearchUserDirectoryResponse userSearchResult;
const UserSearchResultsList({
required this.userSearchResult,
super.key,
});
@override
UserSearchResultsListState createState() => UserSearchResultsListState();
}
class UserSearchResultsListState extends State<UserSearchResultsList> {
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scrollbar(
thumbVisibility: true,
controller: _scrollController,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
itemCount: widget.userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: widget.userSearchResult.results[i].displayName ??
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],
),
),
),
);
}
}
// Pangea#

View file

@ -17,6 +17,7 @@ class NaviRailItem extends StatelessWidget {
// #Pangea
final Color? backgroundColor;
final BorderRadius? borderRadius;
final bool expanded;
// Pangea#
const NaviRailItem({
@ -29,6 +30,7 @@ class NaviRailItem extends StatelessWidget {
// #Pangea
this.backgroundColor,
this.borderRadius,
this.expanded = false,
// Pangea#
super.key,
});
@ -52,91 +54,121 @@ class NaviRailItem extends StatelessWidget {
// #Pangea
// return SizedBox(
// height: 72,
return SizedBox(
height: width - (isColumnMode ? 16.0 : 12.0),
width: width,
// width: FluffyThemes.navRailWidth,
// Pangea#
child: Stack(
children: [
Positioned(
top: 8,
bottom: 8,
left: 0,
child: AnimatedContainer(
width: isSelected
? FluffyThemes.isColumnMode(context)
? 8
: 4
: 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(90),
bottomRight: Radius.circular(90),
),
),
),
),
Center(
child: AnimatedScale(
scale: hovered ? 1.1 : 1.0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
// #Pangea
// child: Material(
// borderRadius: borderRadius,
// color: isSelected
// ? theme.colorScheme.primaryContainer
// : theme.colorScheme.surfaceContainerHigh,
child: UnreadRoomsBadge(
filter: unreadBadgeFilter ?? (_) => false,
badgePosition: BadgePosition.topEnd(
top: -4,
end: isColumnMode ? 8 : 4,
),
child: Container(
alignment: Alignment.center,
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: width - (isColumnMode ? 16.0 : 12.0),
width: width,
// width: FluffyThemes.navRailWidth,
// Pangea#
child: Stack(
children: [
Positioned(
top: 8,
bottom: 8,
left: 0,
child: AnimatedContainer(
width: isSelected
? FluffyThemes.isColumnMode(context)
? 8
: 4
: 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
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,
child: InkWell(
borderRadius: borderRadius,
onTap: onTap,
// #Pangea
child: icon,
// child: unreadBadgeFilter == null
// ? icon
// : UnreadRoomsBadge(
// filter: unreadBadgeFilter,
// badgePosition: BadgePosition.topEnd(
// top: -12,
// end: -8,
// ),
// child: icon,
// ),
// Pangea#
color: theme.colorScheme.primary,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(90),
bottomRight: Radius.circular(90),
),
),
),
),
Center(
child: AnimatedScale(
scale: hovered ? 1.1 : 1.0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
// #Pangea
// child: Material(
// borderRadius: borderRadius,
// color: isSelected
// ? theme.colorScheme.primaryContainer
// : theme.colorScheme.surfaceContainerHigh,
child: UnreadRoomsBadge(
filter: unreadBadgeFilter ?? (_) => false,
badgePosition: BadgePosition.topEnd(
top: 1,
end: isColumnMode ? 8 : 4,
),
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,
),
child: TooltipVisibility(
visible: !expanded,
// Pangea#
child: Tooltip(
message: toolTip,
child: InkWell(
borderRadius: borderRadius,
onTap: onTap,
// #Pangea
child: icon,
// child: unreadBadgeFilter == null
// ? icon
// : UnreadRoomsBadge(
// filter: unreadBadgeFilter,
// badgePosition: BadgePosition.topEnd(
// top: -12,
// end: -8,
// ),
// child: icon,
// ),
// Pangea#
),
),
),
),
),
),
),
],
),
),
if (expanded)
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: ListTile(
title: Text(
toolTip,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium,
),
onTap: onTap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 0.0,
),
),
),
),
],
),
],
);
},
);

View file

@ -83,6 +83,13 @@ class ChatPermissionsSettingsView extends StatelessWidget {
eventsPowerLevels.removeWhere(
(key, value) => excludedEvents.contains(key),
);
final spaceEvents = ['ban', 'invite', 'kick', 'user_default'];
if (room.isSpace) {
powerLevels.removeWhere(
(key, value) => !spaceEvents.contains(key),
);
}
// Pangea#
return Column(
children: [
@ -123,44 +130,50 @@ class ChatPermissionsSettingsView extends StatelessWidget {
room: room,
// Pangea#
),
Divider(color: theme.dividerColor),
ListTile(
title: Text(
L10n.of(context).notifications,
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
// #Pangea
if (!room.isSpace) ...[
// Pangea#
Divider(color: theme.dividerColor),
ListTile(
title: Text(
L10n.of(context).notifications,
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
),
Builder(
builder: (context) {
const key = 'rooms';
final value = powerLevelsContent
.containsKey('notifications')
? powerLevelsContent
.tryGetMap<String, Object?>('notifications')
?.tryGet<int>('rooms') ??
0
: 0;
return PermissionsListTile(
permissionKey: key,
permission: value,
category: 'notifications',
canEdit: room.canChangePowerLevel,
onChanged: (level) => controller.editPowerLevel(
context,
key,
value,
newLevel: level,
Builder(
builder: (context) {
const key = 'rooms';
final value =
powerLevelsContent.containsKey('notifications')
? powerLevelsContent
.tryGetMap<String, Object?>(
'notifications',
)
?.tryGet<int>('rooms') ??
0
: 0;
return PermissionsListTile(
permissionKey: key,
permission: value,
category: 'notifications',
),
// #Pangea
room: room,
// Pangea#
);
},
),
canEdit: room.canChangePowerLevel,
onChanged: (level) => controller.editPowerLevel(
context,
key,
value,
newLevel: level,
category: 'notifications',
),
// #Pangea
room: room,
// Pangea#
);
},
),
],
Divider(color: theme.dividerColor),
ListTile(
title: Text(

View file

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/url_launcher.dart';
@ -200,12 +200,20 @@ class _MessageSearchResultListTile extends StatelessWidget {
icon: const Icon(
Icons.chevron_right_outlined,
),
onPressed: () => context.go(
'/${Uri(
pathSegments: ['rooms', room.id],
queryParameters: {'event': event.eventId},
)}',
// #Pangea
// onPressed: () => context.go(
// '/${Uri(
// pathSegments: ['rooms', room.id],
// queryParameters: {'event': event.eventId},
// )}',
// ),
onPressed: () => NavigationUtil.goToSpaceRoute(
room.id,
[],
context,
queryParams: {'event': event.eventId},
),
// Pangea#
),
);
}

View file

@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/authentication/p_login.dart';
import 'package:fluffychat/pangea/login/pages/login_options_view.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_view.dart';
import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart';
import 'package:fluffychat/pangea/user/utils/p_login.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
@ -43,10 +42,7 @@ class LoginController extends State<Login> {
// #Pangea
bool loadingSignIn = false;
bool loadingAppleSSO = false;
bool loadingGoogleSSO = false;
final PangeaController pangeaController = MatrixState.pangeaController;
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
Client? client;
@ -61,7 +57,7 @@ class LoginController extends State<Login> {
// TODO: implement initState
super.initState();
loadingSignIn = true;
pangeaController.checkHomeServerAction().then((client) {
checkHomeServerAction().then((client) {
if (mounted) {
setState(() {
loadingSignIn = false;
@ -98,17 +94,6 @@ class LoginController extends State<Login> {
}
}
void setLoadingSSO(bool loading, SSOProvider provider) {
if (provider == SSOProvider.apple) {
loadingAppleSSO = loading;
loadingGoogleSSO = false;
} else if (provider == SSOProvider.google) {
loadingGoogleSSO = loading;
loadingAppleSSO = false;
}
if (mounted) setState(() {});
}
void login() async => pLoginAction(controller: this, context: context);
// void login() async {
// final matrix = Matrix.of(context);
@ -234,6 +219,30 @@ class LoginController extends State<Login> {
// if (mounted) setState(() {});
// }
// }
Future<Client> checkHomeServerAction() async {
final client = await Matrix.of(context).getLoginClient();
if (client.homeserver != null) {
await Future.delayed(Duration.zero);
return client;
}
final String homeServer =
AppConfig.defaultHomeserver.trim().toLowerCase().replaceAll(' ', '-');
var homeserver = Uri.parse(homeServer);
if (homeserver.scheme.isEmpty) {
homeserver = Uri.https(homeServer, '');
}
try {
await client.register();
Matrix.of(context).loginRegistrationSupported = true;
} on MatrixException catch (e) {
Matrix.of(context).loginRegistrationSupported =
e.requireAdditionalAuthentication;
}
return client;
}
// Pangea#
void passwordForgotten() async {

View file

@ -13,7 +13,7 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart';
import 'package:fluffychat/pangea/spaces/client_spaces_extension.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/widgets/matrix.dart';

View file

@ -5,7 +5,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/user/utils/p_logout.dart';
import 'package:fluffychat/pangea/authentication/p_logout.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';

View file

@ -102,30 +102,47 @@ class SettingsSecurityController extends State<SettingsSecurity> {
if (mxid == null || mxid.isEmpty || mxid != supposedMxid) {
return;
}
final input = await showTextInputDialog(
useRootNavigator: false,
// #Pangea
// final input = await showTextInputDialog(
// useRootNavigator: false,
// context: context,
// title: L10n.of(context).pleaseEnterYourPassword,
// okLabel: L10n.of(context).ok,
// cancelLabel: L10n.of(context).cancel,
// isDestructive: true,
// obscureText: true,
// hintText: '******',
// minLines: 1,
// maxLines: 1,
// );
// if (input == null) return;
// await showFutureLoadingDialog(
// context: context,
// future: () => Matrix.of(context).client.deactivateAccount(
// auth: AuthenticationPassword(
// password: input,
// identifier: AuthenticationUserIdentifier(
// user: Matrix.of(context).client.userID!,
// ),
// ),
// ),
// );
// Pangea#
final resp = await showFutureLoadingDialog(
context: context,
title: L10n.of(context).pleaseEnterYourPassword,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
isDestructive: true,
obscureText: true,
hintText: '******',
minLines: 1,
maxLines: 1,
);
if (input == null) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.deactivateAccount(
auth: AuthenticationPassword(
password: input,
identifier: AuthenticationUserIdentifier(
user: Matrix.of(context).client.userID!,
delay: false,
future: () =>
Matrix.of(context).client.uiaRequestBackground<IdServerUnbindResult?>(
(auth) => Matrix.of(context).client.deactivateAccount(
auth: auth,
),
),
),
),
);
if (!resp.isError) {
await Matrix.of(context).client.logout();
}
}
void showBootstrapDialog(BuildContext context) async {

View file

@ -59,9 +59,9 @@
// super.initState();
// selectedLanguageOfInstructions =
// MatrixState.pangeaController.languageController.userL1?.langCode;
// MatrixState.pangeaController.userController.userL1?.langCode;
// selectedTargetLanguage =
// MatrixState.pangeaController.languageController.userL2?.langCode;
// MatrixState.pangeaController.userController.userL2?.langCode;
// selectedCefrLevel = LanguageLevelTypeEnum.a1;
// selectedNumberOfParticipants = 3;
// _setMode();
@ -79,7 +79,7 @@
// ActivitySettingRequestSchema get req => ActivitySettingRequestSchema(
// langCode:
// MatrixState.pangeaController.languageController.userL1?.langCode ??
// MatrixState.pangeaController.userController.userL1?.langCode ??
// LanguageKeys.defaultLanguage,
// );
@ -139,9 +139,9 @@
// modeController.clear();
// selectedMedia = MediaEnum.nan;
// selectedLanguageOfInstructions =
// MatrixState.pangeaController.languageController.userL1?.langCode;
// MatrixState.pangeaController.userController.userL1?.langCode;
// selectedTargetLanguage =
// MatrixState.pangeaController.languageController.userL2?.langCode;
// MatrixState.pangeaController.userController.userL2?.langCode;
// selectedCefrLevel = LanguageLevelTypeEnum.a1;
// selectedNumberOfParticipants = 3;
// });

View file

@ -158,7 +158,7 @@
// controller.selectedLanguageOfInstructions!,
// )
// : MatrixState
// .pangeaController.languageController.userL1,
// .pangeaController.userController.userL1,
// isL2List: false,
// decorationText:
// L10n.of(context).languageOfInstructionsLabel,
@ -174,7 +174,7 @@
// controller.selectedTargetLanguage!,
// )
// : MatrixState
// .pangeaController.languageController.userL2,
// .pangeaController.userController.userL2,
// decorationText: L10n.of(context).targetLanguageLabel,
// isL2List: true,
// ),

View file

@ -1,8 +1,12 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/lemmas/lemma.dart';
class ActivityPlanModel {
final String activityId;
@ -80,7 +84,7 @@ class ActivityPlanModel {
instructions: json[ModelKey.activityPlanInstructions],
req: req,
title: json[ModelKey.activityPlanTitle],
description: json[ModelKey.activityPlanDescription] ??
description: json[ModelKey.description] ??
json[ModelKey.activityPlanLearningObjective],
learningObjective: json[ModelKey.activityPlanLearningObjective],
vocab: List<Vocab>.from(
@ -89,11 +93,11 @@ class ActivityPlanModel {
endAt: json[ModelKey.activityPlanEndAt] != null
? DateTime.parse(json[ModelKey.activityPlanEndAt])
: null,
duration: json[ModelKey.activityPlanDuration] != null
duration: json[ModelKey.duration] != null
? Duration(
days: json[ModelKey.activityPlanDuration]['days'] ?? 0,
hours: json[ModelKey.activityPlanDuration]['hours'] ?? 0,
minutes: json[ModelKey.activityPlanDuration]['minutes'] ?? 0,
days: json[ModelKey.duration]['days'] ?? 0,
hours: json[ModelKey.duration]['hours'] ?? 0,
minutes: json[ModelKey.duration]['minutes'] ?? 0,
)
: null,
roles: roles,
@ -109,11 +113,11 @@ class ActivityPlanModel {
ModelKey.activityPlanInstructions: instructions,
ModelKey.activityPlanRequest: req.toJson(),
ModelKey.activityPlanTitle: title,
ModelKey.activityPlanDescription: description,
ModelKey.description: description,
ModelKey.activityPlanLearningObjective: learningObjective,
ModelKey.activityPlanVocab: vocab.map((vocab) => vocab.toJson()).toList(),
ModelKey.activityPlanEndAt: endAt?.toIso8601String(),
ModelKey.activityPlanDuration: {
ModelKey.duration: {
'days': duration?.inDays ?? 0,
'hours': duration?.inHours.remainder(24) ?? 0,
'minutes': duration?.inMinutes.remainder(60) ?? 0,
@ -177,14 +181,29 @@ class Vocab {
factory Vocab.fromJson(Map<String, dynamic> json) {
return Vocab(
lemma: json['lemma'],
lemma: json[ModelKey.lemma],
pos: json['pos'],
);
}
PangeaToken asToken() {
final text = PangeaTokenText(
content: lemma,
length: lemma.characters.length,
offset: 0,
);
return PangeaToken(
text: text,
lemma: Lemma(text: lemma, saveVocab: true, form: lemma),
pos: pos,
morph: {},
);
}
Map<String, dynamic> toJson() {
return {
'lemma': lemma,
ModelKey.lemma: lemma,
'pos': pos,
};
}

View file

@ -1,6 +1,6 @@
import 'package:fluffychat/pangea/activity_generator/media_enum.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart';
class ActivityPlanRequest {
final String topic;
@ -30,12 +30,12 @@ class ActivityPlanRequest {
Map<String, dynamic> toJson() {
return {
ModelKey.activityRequestTopic: topic,
ModelKey.activityRequestMode: mode,
ModelKey.mode: mode,
ModelKey.activityRequestObjective: objective,
ModelKey.activityRequestMedia: media.string,
ModelKey.activityRequestCefrLevel: cefrLevel.string,
ModelKey.activityRequestLanguageOfInstructions: languageOfInstructions,
ModelKey.activityRequestTargetLanguage: targetLanguage,
ModelKey.targetLanguage: targetLanguage,
ModelKey.activityRequestCount: count,
ModelKey.activityRequestNumberOfParticipants: numberOfParticipants,
ModelKey.activityPlanLocation: location,
@ -45,17 +45,17 @@ class ActivityPlanRequest {
factory ActivityPlanRequest.fromJson(Map<String, dynamic> json) =>
ActivityPlanRequest(
topic: json[ModelKey.activityRequestTopic],
mode: json[ModelKey.activityRequestMode],
mode: json[ModelKey.mode],
objective: json[ModelKey.activityRequestObjective],
media: MediaEnum.nan.fromString(json[ModelKey.activityRequestMedia]),
cefrLevel: json[ModelKey.activityRequestCefrLevel] != null
? LanguageLevelTypeEnumExtension.fromString(
? LanguageLevelTypeEnum.fromString(
json[ModelKey.activityRequestCefrLevel],
)
: LanguageLevelTypeEnum.a1,
languageOfInstructions:
json[ModelKey.activityRequestLanguageOfInstructions],
targetLanguage: json[ModelKey.activityRequestTargetLanguage],
targetLanguage: json[ModelKey.targetLanguage],
count: json[ModelKey.activityRequestCount],
numberOfParticipants:
json[ModelKey.activityRequestNumberOfParticipants],

View file

@ -2,8 +2,12 @@ import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:matrix/matrix.dart';
import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_settings_language_icon.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
@ -13,10 +17,12 @@ class ActivityParticipantIndicator extends StatelessWidget {
final String name;
final String? userId;
final User? user;
final Room? room;
final VoidCallback? onTap;
final bool selected;
final bool selectable;
final bool shimmer;
final double opacity;
final EdgeInsetsGeometry? padding;
@ -26,9 +32,11 @@ class ActivityParticipantIndicator extends StatelessWidget {
super.key,
required this.name,
this.user,
this.room,
this.userId,
this.selected = false,
this.selectable = true,
this.shimmer = false,
this.onTap,
this.opacity = 1.0,
this.padding,
@ -46,12 +54,48 @@ class ActivityParticipantIndicator extends StatelessWidget {
? () => showMemberActionsPopupMenu(
context: context,
user: user!,
room: room,
)
: null),
child: AbsorbPointer(
absorbing: !selectable,
child: HoverBuilder(
builder: (context, hovered) {
final avatar = userId != null
? user?.avatarUrl == null ||
user!.avatarUrl!.toString().startsWith("mxc")
? Avatar(
mxContent:
user?.avatarUrl != null ? user!.avatarUrl! : null,
name: userId!.localpart,
size: 60.0,
userId: userId,
miniIcon:
room != null && userId == BotName.byEnvironment
? BotSettingsLanguageIcon(room: room!)
: null,
presenceOffset:
room != null && userId == BotName.byEnvironment
? const Offset(0, 0)
: null,
)
: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: CachedNetworkImage(
imageUrl: user!.avatarUrl!.toString(),
width: 60.0,
height: 60.0,
fit: BoxFit.cover,
),
)
: CircleAvatar(
radius: 30.0,
backgroundColor: theme.colorScheme.primaryContainer,
child: const Icon(
Icons.question_mark,
size: 30.0,
),
);
return Opacity(
opacity: opacity,
child: Container(
@ -64,41 +108,20 @@ class ActivityParticipantIndicator extends StatelessWidget {
borderRadius: borderRadius ?? BorderRadius.circular(8.0),
color: (hovered || selected) && selectable
? theme.colorScheme.surfaceContainerHighest
: Colors.transparent,
: theme.colorScheme.surface.withAlpha(130),
),
constraints: const BoxConstraints(maxWidth: 200.0),
height: 125.0,
constraints: const BoxConstraints(maxWidth: 100.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
userId != null
? user?.avatarUrl == null ||
user!.avatarUrl!.toString().startsWith("mxc")
? Avatar(
mxContent: user?.avatarUrl != null
? user!.avatarUrl!
: null,
name: userId!.localpart,
size: 60.0,
userId: userId,
)
: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: CachedNetworkImage(
imageUrl: user!.avatarUrl!.toString(),
width: 60.0,
height: 60.0,
fit: BoxFit.cover,
),
)
: CircleAvatar(
radius: 30.0,
backgroundColor:
theme.colorScheme.primaryContainer,
child: const Icon(
Icons.question_mark,
size: 30.0,
),
),
shimmer && !selected
? Shimmer.fromColors(
baseColor: AppConfig.gold.withAlpha(20),
highlightColor: AppConfig.gold.withAlpha(50),
child: avatar,
)
: avatar,
Text(
name,
style: const TextStyle(

View file

@ -6,7 +6,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/pangea/spaces/load_participants_builder.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
@ -20,17 +20,19 @@ class ActivityParticipantList extends StatelessWidget {
final bool Function(String)? canSelect;
final bool Function(String)? isSelected;
final bool Function(String)? isShimmering;
final double Function(ActivityRoleModel?)? getOpacity;
const ActivityParticipantList({
super.key,
required this.activity,
required this.assignedRoles,
required this.room,
this.room,
this.course,
this.onTap,
this.canSelect,
this.isSelected,
this.isShimmering,
this.getOpacity,
});
@ -77,6 +79,10 @@ class ActivityParticipantList extends StatelessWidget {
final selectable =
canSelect != null ? canSelect!(availableRole.id) : true;
final shimmering = isShimmering != null
? isShimmering!(availableRole.id)
: false;
return ActivityParticipantIndicator(
name: availableRole.name,
userId: assignedRole?.userId,
@ -87,6 +93,8 @@ class ActivityParticipantList extends StatelessWidget {
: null,
selected: selected,
selectable: selectable,
shimmer: shimmering,
room: room,
);
}).toList(),
),
@ -99,6 +107,7 @@ class ActivityParticipantList extends StatelessWidget {
onTap: () => showMemberActionsPopupMenu(
context: context,
user: member,
room: room,
),
child: Container(
decoration: BoxDecoration(

View file

@ -8,11 +8,9 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_analytics_repo.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
@ -22,7 +20,6 @@ import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extensio
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../activity_summary/activity_summary_repo.dart';
class RoleException implements Exception {
@ -275,7 +272,6 @@ extension ActivityRoomExtension on Room {
activity: activityPlan!,
activityResults: messages,
contentFeedback: [],
analytics: analytics,
roleState: activityRoles,
),
);
@ -312,48 +308,6 @@ extension ActivityRoomExtension on Room {
}
}
Future<ActivitySummaryAnalyticsModel> getActivityAnalytics() async {
// wait for local storage box to init in getAnalytics initialization
if (!MatrixState.pangeaController.getAnalytics.initCompleter.isCompleted) {
await MatrixState.pangeaController.getAnalytics.initCompleter.future;
}
final cached = ActivitySessionAnalyticsRepo.get(id);
final analytics = cached?.analytics ?? ActivitySummaryAnalyticsModel();
DateTime? timestamp = creationTimestamp;
if (cached != null) {
timestamp = cached.lastUseTimestamp;
}
final List<OneConstructUse> uses = [];
for (final use
in MatrixState.pangeaController.getAnalytics.constructListModel.uses) {
final useTimestamp = use.metadata.timeStamp;
if (timestamp != null &&
(useTimestamp == timestamp || useTimestamp.isBefore(timestamp))) {
break;
}
if (use.metadata.roomId != id) continue;
uses.add(use);
}
if (uses.isEmpty) {
return analytics;
}
analytics.addConstructs(client.userID!, uses);
await ActivitySessionAnalyticsRepo.set(
id,
uses.first.metadata.timeStamp,
analytics,
);
return analytics;
}
// UI-related helper functions
bool get showActivityChatUI {

Some files were not shown because too many files have changed in this diff Show more