Merge pull request #738 from pangeachat/737-private-class-code

private class code
This commit is contained in:
ggurdin 2024-10-18 11:57:29 -04:00 committed by GitHub
commit 7aa69b3aa4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 227 additions and 143 deletions

View file

@ -4086,6 +4086,7 @@
}
},
"roomCapacityExplanation": "{roomType} capacity limits the number of non-admins allowed in a room.",
"tooManyRequest": "Too many request, please try again later.",
"@roomCapacityExplanation": {
"type": "text",
"placeholders": {

View file

@ -1,6 +1,7 @@
import 'package:file_picker/file_picker.dart';
import 'package:fluffychat/pages/new_space/new_space_view.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
@ -13,6 +14,7 @@ import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
@ -27,8 +29,8 @@ class NewSpaceController extends State<NewSpace> {
TextEditingController nameController = TextEditingController();
TextEditingController topicController = TextEditingController();
// #Pangea
// bool publicGroup = false;
bool publicGroup = true;
bool publicGroup = false;
// bool publicGroup = true;
// final GlobalKey<RoomRulesState> rulesEditorKey = GlobalKey<RoomRulesState>();
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
// commenting out language settings in spaces for now
@ -147,98 +149,118 @@ class NewSpaceController extends State<NewSpace> {
setState(() {
loading = true;
});
try {
final avatar = this.avatar;
avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar);
final spaceId = await client.createRoom(
// #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
// roomAliasName: publicGroup
// ? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
// : null,
roomAliasName: SpaceCodeUtil.generateSpaceCode(),
// Pangea#
name: nameController.text.trim(),
topic: topicController.text.isEmpty ? null : topicController.text,
// #Pangea
// powerLevelContentOverride: {'events_default': 100},
powerLevelContentOverride: addToSpaceKey.currentState != null
? await ClassChatPowerLevels.powerLevelOverrideForClassChat(
context,
addToSpaceKey.currentState!.parent,
)
: null,
// Pangea#
initialState: [
if (avatar != null)
sdk.StateEvent(
type: sdk.EventTypes.RoomAvatar,
content: {'url': avatarUrl.toString()},
),
// #Pangea
...initialState,
// #Pangea
// try {
await showFutureLoadingDialog(
context: context,
future: () async {
try {
// Pangea#
],
);
// #Pangea
final List<Future<dynamic>> futures = [
Matrix.of(context).client.waitForRoomInSync(spaceId, join: true),
];
if (addToSpaceKey.currentState != null) {
futures.add(addToSpaceKey.currentState!.addSpaces(spaceId));
}
await Future.wait(futures);
final avatar = this.avatar;
avatarUrl ??=
avatar == null ? null : await client.uploadContent(avatar);
final classCode = await SpaceCodeUtil.generateSpaceCode(client);
final spaceId = await client.createRoom(
// #Pangea
preset: publicGroup
? sdk.CreateRoomPreset.publicChat
: sdk.CreateRoomPreset.privateChat,
// #Pangea
creationContent: {'type': RoomCreationTypes.mSpace},
visibility: publicGroup ? sdk.Visibility.public : null,
// #Pangea
// roomAliasName: publicGroup
// ? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
// : null,
// roomAliasName: SpaceCodeUtil.generateSpaceCode(),
// Pangea#
name: nameController.text.trim(),
topic: topicController.text.isEmpty ? null : topicController.text,
// #Pangea
// powerLevelContentOverride: {'events_default': 100},
powerLevelContentOverride: addToSpaceKey.currentState != null
? await ClassChatPowerLevels.powerLevelOverrideForClassChat(
context,
addToSpaceKey.currentState!.parent,
)
: null,
// Pangea#
initialState: [
// #Pangea
...initialState,
if (avatar != null)
sdk.StateEvent(
type: sdk.EventTypes.RoomAvatar,
content: {'url': avatarUrl.toString()},
),
sdk.StateEvent(
type: sdk.EventTypes.RoomJoinRules,
content: {
ModelKey.joinRule: sdk.JoinRules.knock
.toString()
.replaceAll('JoinRules.', ''),
ModelKey.accessCode: classCode,
},
),
// Pangea#
],
// Pangea#
);
// #Pangea
final List<Future<dynamic>> futures = [
Matrix.of(context).client.waitForRoomInSync(spaceId, join: true),
];
if (addToSpaceKey.currentState != null) {
futures.add(addToSpaceKey.currentState!.addSpaces(spaceId));
}
await Future.wait(futures);
final capacity = addCapacityKey.currentState?.capacity;
final space = client.getRoomById(spaceId);
if (capacity != null && space != null) {
space.updateRoomCapacity(capacity);
}
final capacity = addCapacityKey.currentState?.capacity;
final space = client.getRoomById(spaceId);
if (capacity != null && space != null) {
space.updateRoomCapacity(capacity);
}
final Room? room = Matrix.of(context).client.getRoomById(spaceId);
if (room == null) {
ErrorHandler.logError(
e: 'Failed to get new space by id $spaceId',
);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(spaceId);
return;
}
final Room? room = Matrix.of(context).client.getRoomById(spaceId);
if (room == null) {
ErrorHandler.logError(
e: 'Failed to get new space by id $spaceId',
);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(spaceId);
return;
}
GoogleAnalytics.createClass(room.name, room.classCode);
try {
await room.invite(BotName.byEnvironment);
} catch (err) {
ErrorHandler.logError(
e: "Failed to invite pangea bot to space ${room.id}",
);
}
// Pangea#
if (!mounted) return;
// #Pangea
// context.pop<String>(spaceId);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(spaceId);
// Pangea#
} catch (e) {
setState(() {
// #Pangea
// topicError = e.toLocalizedString(context);
// Pangea#
});
} finally {
setState(() {
loading = false;
});
}
GoogleAnalytics.createClass(room.name, room.classCode);
try {
await room.invite(BotName.byEnvironment);
} catch (err) {
ErrorHandler.logError(
e: "Failed to invite pangea bot to space ${room.id}",
);
}
// Pangea#
if (!mounted) return;
// #Pangea
// context.pop<String>(spaceId);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(spaceId);
// Pangea#
} catch (e, s) {
// #Pangea
ErrorHandler.logError(e: e, s: s);
rethrow;
// setState(() {
// topicError = e.toLocalizedString(context);
// });
// Pangea#
} finally {
setState(() {
loading = false;
});
}
},
);
// TODO: Go to spaces
}

View file

@ -0,0 +1 @@
const String noClassCode = 'No class code!';

View file

@ -6,4 +6,5 @@ class PLocalKey {
static const String paywallBackoff = 'paywallBackoff';
static const String messagesSinceUpdate = 'messagesSinceLastUpdate';
static const String completedActivities = 'completedActivities';
static const String justInputtedCode = 'justInputtedCode';
}

View file

@ -123,4 +123,8 @@ class ModelKey {
static const String prevEventId = "prev_event_id";
static const String prevLastUpdated = "prev_last_updated";
// room code
static const String joinRule = "join_rule";
static const String accessCode = "access_code";
}

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
@ -9,15 +9,15 @@ import 'package:fluffychat/pangea/extensions/client_extension/client_extension.d
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/utils/space_code.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import '../../widgets/matrix.dart';
import '../utils/firebase_analytics.dart';
import 'base_controller.dart';
class ClassController extends BaseController {
@ -50,62 +50,88 @@ class ClassController extends BaseController {
);
if (classCode != null) {
await _pangeaController.pStoreService.delete(
PLocalKey.cachedClassCodeToJoin,
isAccountData: false,
);
await joinClasswithCode(
context,
classCode,
).onError(
(error, stackTrace) =>
SpaceCodeUtil.messageSnack(context, ErrorCopy(context, error).body),
);
await _pangeaController.pStoreService.delete(
PLocalKey.cachedClassCodeToJoin,
isAccountData: false,
);
}
}
Future<void> joinClasswithCode(BuildContext context, String classCode) async {
final client = Matrix.of(context).client;
try {
final QueryPublicRoomsResponse queryPublicRoomsResponse =
await Matrix.of(context).client.queryPublicRooms(
limit: 1,
filter: PublicRoomQueryFilter(genericSearchTerm: classCode),
);
final PublicRoomsChunk? classChunk =
queryPublicRoomsResponse.chunk.firstWhereOrNull((element) {
return element.canonicalAlias?.replaceAll("#", "").split(":")[0] ==
classCode;
});
if (classChunk == null) {
final knockResponse = await client.httpClient.post(
Uri.parse(
'${client.homeserver}/_synapse/client/pangea/v1/knock_with_code',
),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${client.accessToken}',
},
body: jsonEncode({'access_code': classCode}),
);
if (knockResponse.statusCode == 429) {
SpaceCodeUtil.messageSnack(
context,
L10n.of(context)!.tooManyRequest,
);
return;
}
if (knockResponse.statusCode != 200) {
SpaceCodeUtil.messageSnack(
context,
L10n.of(context)!.unableToFindClass,
);
return;
}
if (_pangeaController.matrixState.client.rooms
.any((room) => room.id == classChunk.roomId)) {
setActiveSpaceIdInChatListController(classChunk.roomId);
SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass);
final knockResult = jsonDecode(knockResponse.body);
final foundClasses = List<String>.from(knockResult['rooms']);
final alreadyJoined = List<String>.from(knockResult['already_joined']);
if (alreadyJoined.isNotEmpty) {
SpaceCodeUtil.messageSnack(
context,
L10n.of(context)!.alreadyInClass,
);
return;
}
if (foundClasses.isEmpty) {
SpaceCodeUtil.messageSnack(
context,
L10n.of(context)!.unableToFindClass,
);
return;
}
final chosenClassId = foundClasses.first;
if (_pangeaController.matrixState.client.rooms
.any((room) => room.id == chosenClassId)) {
setActiveSpaceIdInChatListController(chosenClassId);
SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass);
return;
} else {
await _pangeaController.pStoreService.save(
PLocalKey.justInputtedCode,
classCode,
isAccountData: false,
);
await client.joinRoomById(chosenClassId);
_pangeaController.pStoreService.delete(PLocalKey.justInputtedCode);
}
await _pangeaController.matrixState.client.joinRoom(classChunk.roomId);
if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) ==
if (_pangeaController.matrixState.client.getRoomById(chosenClassId) ==
null) {
await _pangeaController.matrixState.client.waitForRoomInSync(
classChunk.roomId,
chosenClassId,
join: true,
);
}
// If the room is full, leave
final room =
_pangeaController.matrixState.client.getRoomById(classChunk.roomId);
_pangeaController.matrixState.client.getRoomById(chosenClassId);
if (room == null) {
return;
}
@ -121,12 +147,12 @@ class ClassController extends BaseController {
return;
}
setActiveSpaceIdInChatListController(classChunk.roomId);
setActiveSpaceIdInChatListController(chosenClassId);
// add the user's analytics room to this joined space
// so their teachers can join them via the space hierarchy
final Room? joinedSpace =
_pangeaController.matrixState.client.getRoomById(classChunk.roomId);
_pangeaController.matrixState.client.getRoomById(chosenClassId);
// when possible, add user's analytics room the to space they joined
joinedSpace?.addAnalyticsRoomsToSpace();

View file

@ -5,6 +5,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_code_constants.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';

View file

@ -15,8 +15,14 @@ extension SpaceRoomExtension on Room {
}
return "Not in a class!";
}
return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", "");
final roomJoinRules = getState(EventTypes.RoomJoinRules, "");
if (roomJoinRules != null) {
final accessCode = roomJoinRules.content.tryGet(ModelKey.accessCode);
if (accessCode is String) {
return accessCode;
}
}
return noClassCode;
}
void _checkClass() {

View file

@ -1,6 +1,8 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
@ -17,6 +19,7 @@ void chatListHandleSpaceTap(
ChatListController controller,
Room space,
) {
final PangeaController pangeaController = MatrixState.pangeaController;
void setActiveSpaceAndCloseChat() {
controller.setActiveSpace(space.id);
@ -105,8 +108,13 @@ void chatListHandleSpaceTap(
(element) =>
element.isSpace && element.membership == Membership.join,
);
final justInputtedCode = pangeaController.pStoreService
.read(PLocalKey.justInputtedCode, isAccountData: false);
if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) {
autoJoin(space);
} else if (justInputtedCode != null &&
justInputtedCode == space.classCode) {
// do nothing
} else {
showAlertDialog(context);
}

View file

@ -1,23 +1,38 @@
import 'dart:math';
import 'dart:convert';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../controllers/pangea_controller.dart';
import 'package:matrix/matrix.dart';
class SpaceCodeUtil {
static const codeLength = 6;
static const codeLength = 7;
static bool isValidCode(String? spacecode) {
return spacecode == null || spacecode.length > 4;
if (spacecode == null) return false;
return spacecode.length == codeLength && spacecode.contains(r'[0-9]');
}
static String generateSpaceCode() {
final r = Random();
const chars = 'AaBbCcDdEeFfGgHhiJjKkLMmNnoPpQqRrSsTtUuVvWwXxYyZz1234567890';
return List.generate(codeLength, (index) => chars[r.nextInt(chars.length)])
.join();
static Future<String> generateSpaceCode(Client client) async {
final response = await client.httpClient.get(
Uri.parse(
'${client.homeserver}/_synapse/client/pangea/v1/request_room_code',
),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${client.accessToken}',
},
);
if (response.statusCode != 200) {
throw Exception('Failed to generate room code: $response');
}
final roomCodeResult = jsonDecode(response.body);
if (roomCodeResult['access_code'] is String) {
return roomCodeResult['access_code'] as String;
} else {
throw Exception('Invalid response, access_code not found $response');
}
}
static Future<void> joinWithSpaceCodeDialog(
@ -64,6 +79,7 @@ class SpaceCodeUtil {
SnackBar(
duration: const Duration(seconds: 10),
content: Text(message),
showCloseIcon: true,
),
);
}

View file

@ -1,12 +1,11 @@
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../../widgets/matrix.dart';
import '../../constants/local.key.dart';
//if on home with classcode in url and not logged in, then save it soemhow and after llogin, join class automatically
//if on home with classcode in url and logged in, then join class automatically
class JoinClassWithLink extends StatefulWidget {
@ -19,7 +18,7 @@ class JoinClassWithLink extends StatefulWidget {
//PTODO - show class info in field so they know they're joining the right class
class _JoinClassWithLinkState extends State<JoinClassWithLink> {
String? classCode;
final PangeaController _pangeaController = MatrixState.pangeaController;
final PangeaController pangeaController = MatrixState.pangeaController;
@override
void initState() {
@ -39,8 +38,7 @@ class _JoinClassWithLinkState extends State<JoinClassWithLink> {
);
return;
}
await _pangeaController.pStoreService.save(
await pangeaController.pStoreService.save(
PLocalKey.cachedClassCodeToJoin,
classCode,
isAccountData: false,