Merge branch 'main' of https://github.com/pangeachat/client into remove-duplicate-push

This commit is contained in:
Kelrap 2025-05-22 14:10:19 -04:00
commit c7de9bb1bf
11 changed files with 464 additions and 30 deletions

View file

@ -4937,6 +4937,8 @@
"spaceChildPermission": "Who can add new chats and subspaces to this space",
"addEnvironmentOverride": "Add environment override",
"defaultOption": "Default",
"deleteChatDesc": "Are you sure you want to delete this chat? It will be deleted for all participants and all messages within the chat will no longer be available for practice or learning analytics.",
"deleteSpaceDesc": "The space and any selected chats and/or subspaces will be deleted for all participants and all messages within the chat will no longer be available for practice or learning analytics. This action cannot be undone.",
"chatWithActivities": "Chat with activities",
"findYourPeople": "Find your people",
"launch": "Launch",

View file

@ -32,10 +32,16 @@ class ReplyContent extends StatelessWidget {
timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent;
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
final color = theme.brightness == Brightness.dark
? theme.colorScheme.onTertiaryContainer
: ownMessage
// Pangea#
? ownMessage
? theme.colorScheme.tertiaryContainer
: theme.colorScheme.tertiary;
: theme.colorScheme.onTertiaryContainer
: theme.colorScheme.tertiary;
// ? theme.colorScheme.onTertiaryContainer
// : ownMessage
// ? theme.colorScheme.tertiaryContainer
// : theme.colorScheme.tertiary;
// Pangea#
return Material(
color: Colors.transparent,
@ -69,7 +75,9 @@ class ReplyContent extends StatelessWidget {
fontWeight: FontWeight.bold,
// #Pangea
// color: color,
color: theme.colorScheme.onSurface,
color: ownMessage && theme.brightness == Brightness.dark
? theme.colorScheme.tertiaryContainer
: theme.colorScheme.onSurface,
// Pangea#
fontSize: fontSize,
),

View file

@ -19,6 +19,8 @@ import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
import 'package:fluffychat/pangea/chat_list/utils/app_version_util.dart';
import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
@ -886,7 +888,10 @@ class ChatListController extends State<ChatList>
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.delete_outlined,
// #Pangea
// Icons.delete_outlined,
Icons.logout_outlined,
// Pangea#
color: Theme.of(context).colorScheme.onErrorContainer,
),
const SizedBox(width: 12),
@ -901,6 +906,28 @@ class ChatListController extends State<ChatList>
],
),
),
// #Pangea
if (room.isRoomAdmin)
PopupMenuItem(
value: ChatContextAction.delete,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.delete_outlined,
color: Theme.of(context).colorScheme.onErrorContainer,
),
const SizedBox(width: 12),
Text(
L10n.of(context).delete,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
],
),
),
// Pangea#
],
);
@ -1022,6 +1049,37 @@ class ChatListController extends State<ChatList>
},
);
return;
case ChatContextAction.delete:
if (room.isSpace) {
final resp = await showDialog<bool?>(
context: context,
builder: (_) => DeleteSpaceDialog(space: room),
);
if (resp == true && mounted) {
context.go("/rooms?spaceId=clear");
}
} else {
final confirmed = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).delete,
cancelLabel: L10n.of(context).cancel,
isDestructive: true,
message: room.isSpace
? L10n.of(context).deleteSpaceDesc
: L10n.of(context).deleteChatDesc,
);
if (confirmed != OkCancelResult.ok) return;
if (!mounted) return;
final resp = await showFutureLoadingDialog(
context: context,
future: room.delete,
);
if (resp.isError) return;
if (mounted) context.go("/rooms?spaceId=clear");
}
return;
// Pangea#
case ChatContextAction.block:
final userId =
@ -1296,5 +1354,6 @@ enum ChatContextAction {
block,
// #Pangea
removeFromSpace,
delete,
// Pangea#
}

View file

@ -8,10 +8,12 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/class_name_header.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/conversation_bot/conversation_bot_settings.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/download_space_analytics_button.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/room_capacity_button.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/visibility_toggle.dart';
@ -426,6 +428,58 @@ class PangeaChatDetailsView extends StatelessWidget {
},
),
Divider(color: theme.dividerColor, height: 1),
if (room.isRoomAdmin)
ListTile(
title: Text(
L10n.of(context).delete,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.bold,
),
),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: Icon(
Icons.delete_outline,
color: Theme.of(context).colorScheme.error,
),
),
onTap: () async {
if (room.isSpace) {
final resp = await showDialog<bool?>(
context: context,
builder: (_) =>
DeleteSpaceDialog(space: room),
);
if (resp == true) {
context.go("/rooms?spaceId=clear");
}
} else {
final confirmed = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).delete,
cancelLabel: L10n.of(context).cancel,
isDestructive: true,
message: room.isSpace
? L10n.of(context).deleteSpaceDesc
: L10n.of(context).deleteChatDesc,
);
if (confirmed != OkCancelResult.ok) return;
final resp = await showFutureLoadingDialog(
context: context,
future: room.delete,
);
if (resp.isError) return;
context.go("/rooms?spaceId=clear");
}
},
),
Divider(color: theme.dividerColor, height: 1),
ListTile(
title: Text(
L10n.of(context).countParticipants(

View file

@ -0,0 +1,59 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/matrix_api_lite/generated/api.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
extension on Api {
// Send a POST request to /_synapse/client/pangea/v1/delete_room with JSON body {room_id: string}.
// Response 200 OK format: { message: "Deleted" }.
// Requester must be member of the room and have the highest power level of the room to perform this request.
Future<void> delete(String roomId) async {
final requestUri = Uri(
path: '_synapse/client/pangea/v1/delete_room',
);
final request = Request('POST', baseUri!.resolveUri(requestUri));
request.headers['content-type'] = 'application/json';
request.headers['authorization'] = 'Bearer ${bearerToken!}';
request.bodyBytes = utf8.encode(
jsonEncode({
'room_id': roomId,
}),
);
final response = await httpClient.send(request);
if (response.statusCode != 200) {
throw Exception('http error response');
}
}
}
extension DeleteRoom on Room {
Future<void> delete() async {
await client.delete(id);
}
Future<List<SpaceRoomsChunk>> getSpaceChildrenToDelete() async {
final List<SpaceRoomsChunk> rooms = [];
String? nextBatch;
int calls = 0;
while ((nextBatch != null || calls == 0) && calls < 10) {
final resp = await client.getSpaceHierarchy(
id,
from: nextBatch,
limit: 100,
);
rooms.addAll(resp.rooms);
nextBatch = resp.nextBatch;
calls++;
}
return rooms
.where(
(r) => r.roomType != PangeaRoomTypes.analytics && r.roomId != id,
)
.toList();
}
}

View file

@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
class DeleteSpaceDialog extends StatefulWidget {
final Room space;
const DeleteSpaceDialog({
super.key,
required this.space,
});
@override
State<DeleteSpaceDialog> createState() => DeleteSpaceDialogState();
}
class DeleteSpaceDialogState extends State<DeleteSpaceDialog> {
List<SpaceRoomsChunk> _rooms = [];
final List<SpaceRoomsChunk> _roomsToDelete = [];
bool _loadingRooms = true;
String? _roomLoadError;
bool _deleting = false;
String? _deleteError;
@override
void initState() {
super.initState();
_getSpaceChildrenToDelete();
}
Future<void> _getSpaceChildrenToDelete() async {
setState(() {
_loadingRooms = true;
_roomLoadError = null;
});
try {
_rooms = await widget.space.getSpaceChildrenToDelete();
} catch (e, s) {
_roomLoadError = L10n.of(context).oopsSomethingWentWrong;
ErrorHandler.logError(
e: e,
s: s,
data: {
"roomID": widget.space.id,
},
);
} finally {
setState(() {
_loadingRooms = false;
});
}
}
void _onRoomSelected(
bool? selected,
SpaceRoomsChunk room,
) {
if (selected == null ||
(selected && _roomsToDelete.contains(room)) ||
(!selected && !_roomsToDelete.contains(room))) {
return;
}
setState(() {
selected ? _roomsToDelete.add(room) : _roomsToDelete.remove(room);
});
}
Future<void> _deleteSpace() async {
setState(() {
_deleting = true;
_deleteError = null;
});
try {
final List<Future<void>> deleteFutures = [];
for (final room in _roomsToDelete) {
final roomInstance = widget.space.client.getRoomById(room.roomId);
if (roomInstance != null) {
deleteFutures.add(roomInstance.delete());
}
}
await Future.wait(deleteFutures);
await widget.space.delete();
Navigator.of(context).pop(true);
} catch (e, s) {
_deleteError = L10n.of(context).oopsSomethingWentWrong;
ErrorHandler.logError(
e: e,
s: s,
data: {
"roomID": widget.space.id,
},
);
} finally {
if (mounted) {
setState(() {
_deleting = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
constraints: const BoxConstraints(
maxWidth: 400,
),
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.error,
),
borderRadius: BorderRadius.circular(32.0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
L10n.of(context).areYouSure,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
L10n.of(context).deleteSpaceDesc,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
SizedBox(
height: 300,
child: Builder(
builder: (context) {
if (_loadingRooms) {
return const Center(
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator.adaptive(),
),
);
}
if (_roomLoadError != null) {
return Center(
child: Column(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
),
Text(L10n.of(context).oopsSomethingWentWrong),
],
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ListView.builder(
shrinkWrap: true,
itemCount: _rooms.length,
itemBuilder: (context, index) {
final chunk = _rooms[index];
final room =
widget.space.client.getRoomById(chunk.roomId);
final isMember = room != null &&
room.membership == Membership.join &&
room.isRoomAdmin;
final displayname = chunk.name ??
chunk.canonicalAlias ??
L10n.of(context).emptyChat;
return AnimatedOpacity(
duration: FluffyThemes.animationDuration,
opacity: isMember ? 1 : 0.5,
child: CheckboxListTile(
value: _roomsToDelete.contains(chunk),
onChanged: isMember
? (value) => _onRoomSelected(value, chunk)
: null,
title: Text(displayname),
controlAffinity: ListTileControlAffinity.leading,
),
);
},
),
);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(8.0, 16.0, 8.0, 8.0),
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: OutlinedButton(
onPressed: _deleting ? null : _deleteSpace,
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
side: BorderSide(
color: _deleting
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.error,
),
),
child: _deleting
? const SizedBox(
height: 10,
width: 100,
child: LinearProgressIndicator(),
)
: Text(L10n.of(context).delete),
),
),
OutlinedButton(
onPressed: Navigator.of(context).pop,
child: Text(L10n.of(context).cancel),
),
],
),
),
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: _deleteError != null
? Padding(
padding: const EdgeInsets.all(8.0),
child: Text(L10n.of(context).oopsSomethingWentWrong),
)
: const SizedBox(),
),
],
),
),
);
}
}

View file

@ -15,25 +15,6 @@ enum ActivityTypeEnum {
}
extension ActivityTypeExtension on ActivityTypeEnum {
String get string {
switch (this) {
case ActivityTypeEnum.wordMeaning:
return 'word_meaning';
case ActivityTypeEnum.wordFocusListening:
return 'word_focus_listening';
case ActivityTypeEnum.hiddenWordListening:
return 'hidden_word_listening';
case ActivityTypeEnum.lemmaId:
return 'lemma_id';
case ActivityTypeEnum.emoji:
return 'emoji';
case ActivityTypeEnum.morphId:
return 'morph_id';
case ActivityTypeEnum.messageMeaning:
return 'message_meaning'; // TODO: Add to L10n
}
}
bool get hiddenType {
switch (this) {
case ActivityTypeEnum.wordMeaning:

View file

@ -83,7 +83,7 @@ class MessageActivityRequest {
'message_tokens': messageTokens.map((e) => e.toJson()).toList(),
'activity_quality_feedback': activityQualityFeedback?.toJson(),
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
'target_type': targetType.string,
'target_type': targetType.name,
'target_morph_feature': targetMorphFeature,
};
}

View file

@ -326,7 +326,7 @@ class PracticeActivityModel {
Map<String, dynamic> toJson() {
return {
'lang_code': langCode,
'activity_type': activityType.string,
'activity_type': activityType.name,
'content': multipleChoiceContent?.toJson(),
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
'match_content': matchContent?.toJson(),

View file

@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
@ -60,13 +62,22 @@ class PracticeTarget {
userL2.hashCode;
static PracticeTarget fromJson(Map<String, dynamic> json) {
final type = ActivityTypeEnum.values.firstWhereOrNull(
(v) => json['activityType'] == v.name,
);
if (type == null) {
throw Exception(
"ActivityTypeEnum ${json['activityType']} not found in enum",
);
}
return PracticeTarget(
tokens:
(json['tokens'] as List).map((e) => PangeaToken.fromJson(e)).toList(),
activityType: ActivityTypeEnum.values[json['activityType']],
activityType: type,
morphFeature: json['morphFeature'] == null
? null
: MorphFeaturesEnum.values[json['morphFeature']],
: MorphFeaturesEnumExtension.fromString(json['morphFeature']),
userL2: json['userL2'],
);
}
@ -83,7 +94,7 @@ class PracticeTarget {
//unique condensed deterministic key for local storage
String get storageKey {
return tokens.map((e) => e.text.content).join() +
activityType.string +
activityType.name +
(morphFeature?.name ?? "");
}

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: 4.1.10+1
version: 4.1.10+2
environment:
sdk: ">=3.0.0 <4.0.0"