merge main, resolve conflicts

This commit is contained in:
ggurdin 2024-10-15 10:48:44 -04:00
commit 14bed0f430
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
26 changed files with 284 additions and 92 deletions

View file

@ -4233,5 +4233,9 @@
"reportContentIssueTitle": "Report content issue",
"feedback": "Optional feedback",
"reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we'll try again.",
"clickTheWordAgainToDeselect": "Click the selected word to deselect it."
"clickTheWordAgainToDeselect": "Click the selected word to deselect it.",
"l2SupportNa": "Not Available",
"l2SupportAlpha": "Alpha",
"l2SupportBeta": "Beta",
"l2SupportFull": "Full"
}

View file

@ -470,6 +470,14 @@ class ChatController extends State<ChatPageWithRoom>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// #Pangea
// On iOS, if the toolbar is open and the app is closed, then the user goes
// back to do more toolbar activities, the toolbar buttons / selection don't
// update properly. So, when the user closes the app, close the toolbar overlay.
if (state == AppLifecycleState.paused) {
clearSelectedEvents();
}
// Pangea#
if (state != AppLifecycleState.resumed) return;
setReadMarker();
}

View file

@ -391,18 +391,19 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
// #Pangea
// const SizedBox(width: 8),
const SizedBox(width: 5),
// SizedBox(
// width: 36,
// child:
// Pangea#
SizedBox(
width: 36,
child: Text(
statusText,
style: TextStyle(
color: widget.color,
fontSize: 12,
),
Text(
statusText,
style: TextStyle(
color: widget.color,
fontSize: 12,
),
),
// #Pangea
// ),
// const SizedBox(width: 8),
// Badge(
// isLabelVisible: audioPlayer != null,

View file

@ -384,9 +384,12 @@ class _SpaceViewState extends State<SpaceView> {
} else {
roomId = await client.createGroupChat(
groupName: names.first,
preset: activeSpace.joinRules == JoinRules.public
? CreateRoomPreset.publicChat
: CreateRoomPreset.privateChat,
// #Pangea
// preset: activeSpace.joinRules == JoinRules.public
// ? CreateRoomPreset.publicChat
// : CreateRoomPreset.privateChat,
preset: CreateRoomPreset.publicChat,
// Pangea#
visibility: activeSpace.joinRules == JoinRules.public
? sdk.Visibility.public
: sdk.Visibility.private,

View file

@ -152,9 +152,12 @@ class NewSpaceController extends State<NewSpace> {
avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar);
final spaceId = await client.createRoom(
preset: publicGroup
? sdk.CreateRoomPreset.publicChat
: sdk.CreateRoomPreset.privateChat,
// #Pangea
// preset: publicGroup
// ? sdk.CreateRoomPreset.publicChat
// : sdk.CreateRoomPreset.privateChat,
preset: sdk.CreateRoomPreset.publicChat,
// Pangea#
creationContent: {'type': RoomCreationTypes.mSpace},
visibility: publicGroup ? sdk.Visibility.public : null,
// #Pangea

View file

@ -255,7 +255,12 @@ class UserBottomSheetController extends State<UserBottomSheet> {
sendError = null;
});
try {
final roomId = await client.startDirectChat(userId);
final roomId = await client.startDirectChat(
userId,
// #Pangea
enableEncryption: false,
// Pangea#
);
if (!mounted) return;
final room = client.getRoomById(roomId);
if (room == null) {

View file

@ -105,6 +105,7 @@ class IgcController {
return;
}
choreographer.chatController.inputFocus.unfocus();
OverlayUtil.showPositionedCard(
context: context,
cardToShow: SpanCard(
@ -124,8 +125,8 @@ class IgcController {
),
roomId: choreographer.roomId,
),
maxHeight: match.isITStart ? 260 : 400,
maxWidth: match.isITStart ? 350 : 400,
maxHeight: match.isITStart ? 260 : 350,
maxWidth: 350,
transformTargetId: choreographer.inputTransformTargetKey,
);
}

View file

@ -308,6 +308,8 @@ class ITChoices extends StatelessWidget {
);
return;
}
controller.choreographer.chatController.inputFocus.unfocus();
OverlayUtil.showPositionedCard(
context: context,
cardToShow: choiceFeedback == null

View file

@ -1,4 +1,5 @@
class BotMode {
static const direct = "direct";
static const discussion = "discussion";
static const custom = "custom";
static const storyGame = "story_game";

View file

@ -27,7 +27,9 @@ class PangeaLanguage {
static Future<void> initialize() async {
try {
_langList = await _getCachedFlags();
if (await _shouldFetch || _langList.isEmpty) {
if (await _shouldFetch ||
_langList.isEmpty ||
_langList.every((lang) => !lang.l2)) {
_langList = await LanguageRepo.fetchLanguages();
await _saveFlags(_langList);

View file

@ -20,7 +20,6 @@ import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/controllers/user_controller.dart';
import 'package:fluffychat/pangea/controllers/word_net_controller.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
@ -193,19 +192,53 @@ class PangeaController {
void startChatWithBotIfNotPresent() {
Future.delayed(const Duration(milliseconds: 10000), () async {
// check if user is logged in
if (!matrixState.client.isLogged() ||
(await matrixState.client.hasBotDM)) {
if (!matrixState.client.isLogged()) {
return;
}
try {
await matrixState.client.startDirectChat(
BotName.byEnvironment,
enableEncryption: false,
);
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
const List<Room> botDMs = [];
for (final room in matrixState.client.rooms) {
if (await room.isBotDM) {
botDMs.add(room);
}
}
if (botDMs.isEmpty) {
try {
await matrixState.client.startDirectChat(
BotName.byEnvironment,
enableEncryption: false,
);
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
return;
}
final Room botDMWithLatestActivity = botDMs.reduce((a, b) {
if (a.timeline == null || b.timeline == null) {
return a;
}
final aLastEvent = a.timeline!.events.last;
final bLastEvent = b.timeline!.events.last;
return aLastEvent.originServerTs.isAfter(bLastEvent.originServerTs)
? a
: b;
});
for (final room in botDMs) {
if (room.id != botDMWithLatestActivity.id) {
await room.leave();
continue;
}
}
final participants = await botDMWithLatestActivity.requestParticipants();
final joinedParticipants =
participants.where((e) => e.membership == Membership.join).toList();
if (joinedParticipants.length < 2) {
await botDMWithLatestActivity.invite(BotName.byEnvironment);
}
});
}

View file

@ -118,6 +118,11 @@ class PracticeGenerationController {
requestModel: req,
);
if (res.finished) {
debugPrint('Activity generation finished');
return null;
}
// if the server points to an existing event, return that event
if (res.existingActivityEventId != null) {
final Event? existingEvent =

View file

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum L2SupportEnum {
na,
alpha,
beta,
full,
}
extension L2SupportEnumExtension on L2SupportEnum {
String get storageString {
switch (this) {
case L2SupportEnum.na:
return 'na';
case L2SupportEnum.alpha:
return 'alpha';
case L2SupportEnum.beta:
return 'beta';
case L2SupportEnum.full:
return 'full';
}
}
L2SupportEnum fromStorageString(String storageString) {
switch (storageString) {
case 'na':
case 'L2SupportEnum.na':
return L2SupportEnum.na;
case 'alpha':
case 'L2SupportEnum.alpha':
return L2SupportEnum.alpha;
case 'beta':
case 'L2SupportEnum.beta':
return L2SupportEnum.beta;
case 'full':
case 'L2SupportEnum.full':
return L2SupportEnum.full;
default:
throw Exception('Unknown L2SupportEnum storage string: $storageString');
}
}
String toLocalizedString(BuildContext context) {
final l10n = L10n.of(context)!;
switch (this) {
case L2SupportEnum.na:
return l10n.l2SupportNa;
case L2SupportEnum.alpha:
return l10n.l2SupportAlpha;
case L2SupportEnum.beta:
return l10n.l2SupportBeta;
case L2SupportEnum.full:
return l10n.l2SupportFull;
}
}
Badge toBadge(BuildContext context) {
final theme = Theme.of(context);
Color color;
String label;
switch (this) {
case L2SupportEnum.na:
color = theme.colorScheme.onSurface.withOpacity(0.4); // Muted grey
label = toLocalizedString(context);
break;
case L2SupportEnum.alpha:
color = theme.colorScheme.primary.withOpacity(0.4); // Subtle primary
label = toLocalizedString(context);
break;
case L2SupportEnum.beta:
color =
theme.colorScheme.secondary.withOpacity(0.4); // Subtle secondary
label = toLocalizedString(context);
break;
case L2SupportEnum.full:
color = theme.colorScheme.tertiary.withOpacity(0.4); // Subtle tertiary
label = toLocalizedString(context);
break;
}
return Badge(
label: Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.8), // Dimmed text
fontWeight: FontWeight.w500,
),
),
backgroundColor: color,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
smallSize: 20, // A smaller badge for subtlety
);
}
}

View file

@ -4,6 +4,7 @@ import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/bot_mode.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
@ -253,7 +254,7 @@ extension PangeaRoom on Room {
// bool isMadeForLang(String langCode) => _isMadeForLang(langCode);
Future<bool> get isBotRoom async => await _isBotRoom;
Future<bool> get botIsInRoom async => await _botIsInRoom;
Future<bool> get isBotDM async => await _isBotDM;

View file

@ -49,15 +49,14 @@ extension RoomInformationRoomExtension on Room {
// creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
// }
Future<bool> get _isBotRoom async {
Future<bool> get _botIsInRoom async {
final List<User> participants = await requestParticipants();
return participants.any(
(User user) => user.id == BotName.byEnvironment,
);
}
Future<bool> get _isBotDM async =>
(await isBotRoom) && getParticipants().length == 2;
Future<bool> get _isBotDM async => botOptions?.mode == BotMode.direct;
bool get _isLocked {
if (isDirectChat) return false;

View file

@ -1,6 +1,7 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/enum/l2_support_enum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -12,15 +13,15 @@ class LanguageModel {
final String languageFlag;
final String displayName;
final String? languageEmoji;
final bool l2;
final bool l1;
final L2SupportEnum l2Support;
LanguageModel({
required this.langCode,
required this.languageFlag,
required this.displayName,
required this.l2,
required this.l1,
this.l2Support = L2SupportEnum.na,
this.languageEmoji,
});
@ -37,9 +38,11 @@ class LanguageModel {
displayName: _LanguageLocal.getDisplayName(
code != LanguageKeys.unknownLanguage ? code : json['language_name'],
),
l2: json["l2"] ?? code.contains("es") || code.contains("en"),
l1: json["l1"] ?? !code.contains("es") && !code.contains("en"),
languageEmoji: json['language_emoji'],
l2Support: json['l2_support'] != null
? L2SupportEnum.na.fromStorageString(json['l2_support'])
: L2SupportEnum.na,
);
}
@ -47,11 +50,13 @@ class LanguageModel {
'language_code': langCode,
'language_name': displayName,
'language_flag': languageFlag,
'l2': l2,
'l1': l1,
'language_emoji': languageEmoji,
'l2_support': l2Support.storageString,
};
bool get l2 => l2Support != L2SupportEnum.na;
// Discuss with Jordan - adding langCode field to language objects as separate from displayName
static String codeFromNameOrCode(String codeOrName, [String? url]) {
if (codeOrName.isEmpty) return LanguageKeys.unknownLanguage;
@ -73,7 +78,6 @@ class LanguageModel {
langCode: LanguageKeys.unknownLanguage,
languageFlag: "",
displayName: "Unknown",
l2: false,
l1: false,
);
@ -81,16 +85,12 @@ class LanguageModel {
displayName: context != null
? L10n.of(context)!.multiLingualSpace
: "Multilingual Space",
l2: false,
l1: false,
langCode: LanguageKeys.multiLanguage,
languageFlag: 'assets/colors.png',
languageEmoji: "🌎",
);
// Discuss with Jordan
bool get hasContextualDefinitionSupport => l2;
String? getDisplayName(BuildContext context) {
switch (langCode) {
case 'ab':

View file

@ -156,6 +156,20 @@ class ActivityQualityFeedback {
'bad_activity': badActivity.toJson(),
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ActivityQualityFeedback &&
other.feedbackText == feedbackText &&
other.badActivity == badActivity;
}
@override
int get hashCode {
return feedbackText.hashCode ^ badActivity.hashCode;
}
}
class MessageActivityRequest {
@ -231,7 +245,9 @@ class MessageActivityRequest {
@override
int get hashCode {
return messageId.hashCode ^ const ListEquality().hash(tokensWithXP);
return messageId.hashCode ^
const ListEquality().hash(tokensWithXP) ^
activityQualityFeedback.hashCode;
}
}

View file

@ -234,6 +234,7 @@ class LanguageSelectionRow extends StatelessWidget {
targetLanguage: isSource ? null : language,
);
},
isL2List: !isSource,
initialLanguage: isSource
? controller.sourceLanguageSearch
: controller.targetLanguageSearch,

View file

@ -40,13 +40,15 @@ class InlineTooltip extends StatelessWidget {
const SizedBox(width: 8),
// Text in the middle
Expanded(
child: Text(
instructionsEnum.body(context),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
child: Center(
child: Text(
instructionsEnum.body(context),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
),
textAlign: TextAlign.left,
),
textAlign: TextAlign.left,
),
),
// Close button on the right

View file

@ -19,7 +19,7 @@ import 'package:flutter/material.dart';
const double minCardHeight = 70;
class MessageToolbar extends StatefulWidget {
class MessageToolbar extends StatelessWidget {
final PangeaMessageEvent pangeaMessageEvent;
final MessageOverlayController overLayController;
@ -29,55 +29,45 @@ class MessageToolbar extends StatefulWidget {
required this.overLayController,
});
@override
MessageToolbarState createState() => MessageToolbarState();
}
class MessageToolbarState extends State<MessageToolbar> {
@override
void initState() {
super.initState();
}
Widget get toolbarContent {
final bool subscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
if (!subscribed) {
return MessageUnsubscribedCard(
controller: widget.overLayController,
controller: overLayController,
);
}
switch (widget.overLayController.toolbarMode) {
switch (overLayController.toolbarMode) {
case MessageMode.translation:
return MessageTranslationCard(
messageEvent: widget.pangeaMessageEvent,
selection: widget.overLayController.selectedSpan,
messageEvent: pangeaMessageEvent,
selection: overLayController.selectedSpan,
);
case MessageMode.textToSpeech:
return MessageAudioCard(
messageEvent: widget.pangeaMessageEvent,
overlayController: widget.overLayController,
messageEvent: pangeaMessageEvent,
overlayController: overLayController,
);
case MessageMode.speechToText:
return MessageSpeechToTextCard(
messageEvent: widget.pangeaMessageEvent,
messageEvent: pangeaMessageEvent,
);
case MessageMode.definition:
if (!widget.overLayController.isSelection) {
if (!overLayController.isSelection) {
return const SelectToDefine();
} else {
try {
final selectedText = widget.overLayController.targetText;
final selectedText = overLayController.targetText;
return WordDataCard(
word: selectedText,
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
fullText: widget.pangeaMessageEvent.messageDisplayText,
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
wordLang: pangeaMessageEvent.messageDisplayLangCode,
fullText: pangeaMessageEvent.messageDisplayText,
fullTextLang: pangeaMessageEvent.messageDisplayLangCode,
hasInfo: true,
room: widget.overLayController.widget.chatController.room,
room: overLayController.widget.chatController.room,
);
} catch (e, s) {
debugger(when: kDebugMode);
@ -85,8 +75,8 @@ class MessageToolbarState extends State<MessageToolbar> {
e: "Error in WordDataCard",
s: s,
data: {
"word": widget.overLayController.targetText,
"fullText": widget.pangeaMessageEvent.messageDisplayText,
"word": overLayController.targetText,
"fullText": pangeaMessageEvent.messageDisplayText,
},
);
return const SizedBox();
@ -94,30 +84,25 @@ class MessageToolbarState extends State<MessageToolbar> {
}
case MessageMode.practiceActivity:
return PracticeActivityCard(
pangeaMessageEvent: widget.pangeaMessageEvent,
overlayController: widget.overLayController,
pangeaMessageEvent: pangeaMessageEvent,
overlayController: overLayController,
);
default:
debugger(when: kDebugMode);
ErrorHandler.logError(
e: "Invalid toolbar mode",
s: StackTrace.current,
data: {"newMode": widget.overLayController.toolbarMode},
data: {"newMode": overLayController.toolbarMode},
);
return const SizedBox();
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
key: MatrixState.pAnyState
.layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar')
.layerLinkAndKey('${pangeaMessageEvent.eventId}-toolbar')
.key,
type: MaterialType.transparency,
child: Row(

View file

@ -46,7 +46,7 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
botOptions = widget.room?.botOptions != null
? BotOptionsModel.fromJson(widget.room?.botOptions?.toJson())
: BotOptionsModel();
widget.room?.isBotRoom.then((bool isBotRoom) {
widget.room?.botIsInRoom.then((bool isBotRoom) {
setState(() {
addBot = isBotRoom;
});
@ -260,7 +260,7 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
botOptions = botOptions;
});
final bool isBotRoomMember =
await widget.room?.isBotRoom ?? false;
await widget.room?.botIsInRoom ?? false;
if (addBot && !isBotRoomMember) {
await widget.room?.invite(BotName.byEnvironment);
} else if (!addBot && isBotRoomMember) {

View file

@ -231,6 +231,10 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
return;
}
// clear the current activity and record
currentActivity = null;
currentCompletionRecord = null;
_fetchNewActivity(
ActivityQualityFeedback(
feedbackText: feedback,
@ -248,12 +252,13 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
'record': currentCompletionRecord,
},
);
// clear the current activity and record
currentActivity = null;
currentCompletionRecord = null;
widget.overlayController.exitPracticeFlow();
});
// clear the current activity and record
currentActivity = null;
currentCompletionRecord = null;
}
RepresentationEvent? get representation =>

View file

@ -63,6 +63,7 @@ Future<void> pLanguageDialog(
setState(() => selectedSourceLanguage = p0),
initialLanguage: selectedSourceLanguage,
languages: pangeaController.pLanguageStore.baseOptions,
isL2List: false,
),
PQuestionContainer(
title: L10n.of(parentContext)!.whatLanguageYouWantToLearn,
@ -72,6 +73,7 @@ Future<void> pLanguageDialog(
setState(() => selectedTargetLanguage = p0),
initialLanguage: selectedTargetLanguage,
languages: pangeaController.pLanguageStore.targetOptions,
isL2List: true,
),
],
),

View file

@ -1,5 +1,6 @@
// Flutter imports:
import 'package:fluffychat/pangea/enum/l2_support_enum.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:flutter/material.dart';
@ -10,6 +11,7 @@ class PLanguageDropdown extends StatefulWidget {
final LanguageModel initialLanguage;
final Function(LanguageModel) onChange;
final bool showMultilingual;
final bool isL2List;
const PLanguageDropdown({
super.key,
@ -17,6 +19,7 @@ class PLanguageDropdown extends StatefulWidget {
required this.onChange,
required this.initialLanguage,
this.showMultilingual = false,
required this.isL2List,
});
@override
@ -98,6 +101,7 @@ class _PLanguageDropdownState extends State<PLanguageDropdown> {
value: LanguageModel.multiLingual(context),
child: LanguageDropDownEntry(
languageModel: LanguageModel.multiLingual(context),
isL2List: widget.isL2List,
),
),
...sortedLanguages.map(
@ -105,6 +109,7 @@ class _PLanguageDropdownState extends State<PLanguageDropdown> {
value: languageModel,
child: LanguageDropDownEntry(
languageModel: languageModel,
isL2List: widget.isL2List,
),
),
),
@ -118,9 +123,11 @@ class _PLanguageDropdownState extends State<PLanguageDropdown> {
class LanguageDropDownEntry extends StatelessWidget {
final LanguageModel languageModel;
final bool isL2List;
const LanguageDropDownEntry({
super.key,
required this.languageModel,
required this.isL2List,
});
@override
@ -144,6 +151,9 @@ class LanguageDropDownEntry extends StatelessWidget {
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
const SizedBox(width: 10),
if (isL2List && languageModel.l2Support != L2SupportEnum.full)
languageModel.l2Support.toBadge(context),
],
),
);

View file

@ -21,6 +21,11 @@ extension IsStateExtension on Event {
(isState || !AppConfig.hideAllStateEvents) &&
// #Pangea
content.tryGet(ModelKey.transcription) == null &&
// if sending of transcription fails,
// don't show it as a errored audio event in timeline.
((unsigned?['extra_content']
as Map<String, dynamic>?)?[ModelKey.transcription] ==
null) &&
// hide unimportant state events
(!AppConfig.hideUnimportantStateEvents ||
!isState ||

View file

@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
# Pangea#
publish_to: none
# On version bump also increase the build number for F-Droid
version: 1.21.4+3539
version: 1.21.4+3540
environment:
sdk: ">=3.0.0 <4.0.0"