feat: use cached space code to join space on create account (#4224)

This commit is contained in:
ggurdin 2025-10-02 11:51:25 -04:00 committed by GitHub
parent 343a27e80a
commit 828dbab9a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 308 additions and 675 deletions

View file

@ -53,7 +53,6 @@ import 'package:fluffychat/pangea/login/pages/public_trip_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_alias.dart';
import 'package:fluffychat/pangea/spaces/utils/join_with_link.dart';
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
import 'package:fluffychat/widgets/config_viewer.dart';
@ -289,14 +288,6 @@ abstract class AppRoutes {
),
),
),
GoRoute(
path: '/join_with_alias',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
),
),
// Pangea#
ShellRoute(
// Never use a transition on the shell route. Changing the PageBuilder

View file

@ -581,7 +581,7 @@ class ChatListController extends State<ChatList>
);
final String? justInputtedCode =
MatrixState.pangeaController.classController.justInputtedCode();
MatrixState.pangeaController.spaceCodeController.justInputtedCode;
final newSpaceCode = space?.classCode;
if (newSpaceCode?.toLowerCase() == justInputtedCode?.toLowerCase()) {
return;
@ -1103,7 +1103,8 @@ class ChatListController extends State<ChatList>
void _initPangeaControllers(Client client) {
MatrixState.pangeaController.initControllers();
if (mounted) {
MatrixState.pangeaController.classController.joinCachedSpaceCode(context);
MatrixState.pangeaController.spaceCodeController
.joinCachedSpaceCode(context);
}
}
// Pangea#

View file

@ -76,7 +76,7 @@ void chatListHandleSpaceTap(
element.isSpace && element.membership == Membership.join,
);
final justInputtedCode =
MatrixState.pangeaController.classController.justInputtedCode();
MatrixState.pangeaController.spaceCodeController.justInputtedCode;
if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) {
autoJoin(space);
} else if (justInputtedCode != null &&

View file

@ -1,95 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
class ReferFriendsDialog extends StatelessWidget {
final Room room;
const ReferFriendsDialog({
required this.room,
super.key,
});
@override
Widget build(BuildContext context) {
final inviteLink =
"${Environment.frontendURL}/#/join_with_alias?alias=${Uri.encodeComponent(room.id)}";
return Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(20.0),
border: Border.all(
color: Theme.of(context).colorScheme.primary.withAlpha(50),
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16.0,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: 450,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendDialogAsset}",
errorWidget: (context, url, error) => const SizedBox(),
placeholder: (context, url) => const Center(
child: CircularProgressIndicator.adaptive(),
),
),
),
),
),
Text(
L10n.of(context).referFriendDialogTitle,
style: Theme.of(context).textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
Text(
L10n.of(context).referFriendDialogDesc,
style: Theme.of(context).textTheme.titleMedium,
),
Material(
color: Colors.transparent, // Keeps the `Container`'s background
child: ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.0),
side: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
title: Text(
inviteLink,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.copy_outlined),
onTap: () async {
Clipboard.setData(
ClipboardData(text: inviteLink),
);
},
),
),
],
),
);
}
}

View file

@ -1,7 +1,6 @@
class PLocalKey {
static const String access = "access";
static const String cachedClassCodeToJoin = "cachedclasscodetojoin";
static const String cachedAliasToJoin = "cachedAliasToJoin";
static const String cachedSpaceCodeToJoin = "cachedclasscodetojoin";
static const String beganWebPayment = "beganWebPayment";
static const String dismissedPaywall = 'dismissedPaywall';
static const String paywallBackoff = 'paywallBackoff';

View file

@ -19,7 +19,7 @@ import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/learning_settings/controllers/language_controller.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/spaces/controllers/space_controller.dart';
import 'package:fluffychat/pangea/spaces/controllers/space_code_controller.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/toolbar/controllers/speech_to_text_controller.dart';
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
@ -33,7 +33,7 @@ class PangeaController {
///pangeaControllers
late UserController userController;
late LanguageController languageController;
late ClassController classController;
late SpaceCodeController spaceCodeController;
late PermissionsController permissionsController;
late GetAnalyticsController getAnalytics;
late PutAnalyticsController putAnalytics;
@ -81,7 +81,7 @@ class PangeaController {
_addRefInObjects() {
userController = UserController(this);
languageController = LanguageController(this);
classController = ClassController(this);
spaceCodeController = SpaceCodeController(this);
permissionsController = PermissionsController(this);
getAnalytics = GetAnalyticsController(this);
putAnalytics = PutAnalyticsController(this);
@ -115,7 +115,6 @@ class PangeaController {
'morph_meaning_storage',
'practice_record_cache',
'practice_selection_cache',
'class_storage',
'subscription_storage',
'vocab_storage',
'onboarding_storage',

View file

@ -73,7 +73,7 @@ class CreatePangeaAccountPageState extends State<CreatePangeaAccountPage> {
final l2Set = await MatrixState.pangeaController.userController.isUserL2Set;
if (l2Set) {
context.go('/registration/course');
_onProfileCreated();
return;
}
@ -111,7 +111,7 @@ class CreatePangeaAccountPageState extends State<CreatePangeaAccountPage> {
);
await MatrixState.pangeaController.subscriptionController.reinitialize();
context.go('/registration/course');
await _onProfileCreated();
} catch (err) {
if (err is MatrixException) {
_profileError = err.errorMessage;
@ -125,10 +125,17 @@ class CreatePangeaAccountPageState extends State<CreatePangeaAccountPage> {
}
}
Future<void> _onProfileCreated() async {
final joinedSpaceId = await MatrixState.pangeaController.spaceCodeController
.joinCachedSpaceCode(context);
if (joinedSpaceId != null) return;
context.go('/registration/course');
}
@override
Widget build(BuildContext context) {
if (_loadingProfile && _profileError != null) {
context.go('/registration/course');
_onProfileCreated();
}
return Scaffold(

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -35,10 +37,15 @@ class PrivateTripPageState extends State<PrivateTripPage> {
return;
}
await MatrixState.pangeaController.classController.joinClasswithCode(
final spaceId = await MatrixState.pangeaController.spaceCodeController
.joinSpaceWithCode(
context,
_code,
);
if (spaceId != null) {
context.go('/rooms/spaces/$spaceId/details');
}
}
@override

View file

@ -1,76 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/login/pages/space_code_onboarding_view.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/widgets/matrix.dart';
class SpaceCodeOnboarding extends StatefulWidget {
const SpaceCodeOnboarding({super.key});
@override
State<SpaceCodeOnboarding> createState() => SpaceCodeOnboardingState();
}
class SpaceCodeOnboardingState extends State<SpaceCodeOnboarding> {
Profile? profile;
Client get client => Matrix.of(context).client;
final TextEditingController codeController = TextEditingController();
@override
void initState() {
_setProfile();
codeController.addListener(() {
if (mounted) setState(() {});
});
super.initState();
}
@override
void dispose() {
codeController.dispose();
super.dispose();
}
Future<void> _setProfile() async {
try {
profile = await client.getProfileFromUserId(
client.userID!,
);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'userId': client.userID,
},
);
} finally {
if (mounted) setState(() {});
}
}
Future<void> submitCode() async {
String code = codeController.text.trim();
if (code.isEmpty) return;
try {
final link = Uri.parse(Uri.parse(code).fragment);
if (link.queryParameters.containsKey(SpaceConstants.classCode)) {
code = link.queryParameters[SpaceConstants.classCode]!;
}
} catch (e) {
debugPrint("Text input is not a URL: $e");
}
await MatrixState.pangeaController.classController
.joinClasswithCode(context, code);
}
@override
Widget build(BuildContext context) =>
SpaceCodeOnboardingView(controller: this);
}

View file

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart';
import 'package:fluffychat/pangea/login/pages/space_code_onboarding.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'package:fluffychat/pangea/user/utils/p_logout.dart';
class SpaceCodeOnboardingView extends StatelessWidget {
final SpaceCodeOnboardingState controller;
const SpaceCodeOnboardingView({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
return PangeaLoginScaffold(
customAppBar: AppBar(
leading: BackButton(
onPressed: () => pLogoutAction(
context,
bypassWarning: true,
),
),
),
showAppName: false,
mainAssetUrl: controller.profile?.avatarUrl,
children: [
Text(
L10n.of(context).welcomeUser(
controller.profile?.displayName ??
controller.client.userID?.localpart ??
"",
),
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8.0),
Text(
L10n.of(context).joinSpaceOnboardingDesc,
textAlign: TextAlign.center,
),
const SizedBox(height: 8.0),
FullWidthTextField(
hintText: L10n.of(context).enterCodeToJoin,
controller: controller.codeController,
onSubmitted: (_) => controller.submitCode,
),
FullWidthButton(
title: L10n.of(context).join,
onPressed: controller.submitCode,
enabled: controller.codeController.text.isNotEmpty,
),
const SizedBox(height: 8.0),
TextButton(
child: Text(L10n.of(context).skipForNow),
onPressed: () => context.go("/rooms"),
),
],
);
}
}

View file

@ -244,7 +244,7 @@ class UserSettingsState extends State<UserSettingsPage> {
await _pangeaController.subscriptionController.reinitialize();
context.go(
_pangeaController.classController.cachedClassCode == null
_pangeaController.spaceCodeController.cachedSpaceCode == null
? '/user_age/join_space'
: '/rooms',
);

View file

@ -83,13 +83,13 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
bool get _isKnockRoom => widget.chunk?.joinRule == 'knock';
Future<void> _joinWithCode() async {
final resp =
await MatrixState.pangeaController.classController.joinClasswithCode(
final resp = await MatrixState.pangeaController.spaceCodeController
.joinSpaceWithCode(
context,
_codeController.text,
notFoundError: L10n.of(context).notTheCodeError,
);
if (!resp.isError) {
if (resp != null) {
Navigator.of(context).pop(true);
}
}

View file

@ -0,0 +1,160 @@
// ignore_for_file: implementation_imports
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:get_storage/get_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/spaces/utils/knock_space_extension.dart';
import 'package:fluffychat/pangea/spaces/widgets/too_many_requests_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import '../../common/controllers/base_controller.dart';
class SpaceCodeController extends BaseController {
late PangeaController _pangeaController;
static final GetStorage _spaceStorage = GetStorage('class_storage');
SpaceCodeController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
Future<void> cacheSpaceCode(String code) async {
if (code.isEmpty) return;
await _spaceStorage.write(
PLocalKey.cachedSpaceCodeToJoin,
code,
);
}
String? get justInputtedCode =>
_spaceStorage.read(PLocalKey.justInputtedCode);
String? get cachedSpaceCode =>
_spaceStorage.read(PLocalKey.cachedSpaceCodeToJoin);
Future<String?> joinCachedSpaceCode(BuildContext context) async {
final String? spaceCode = cachedSpaceCode;
if (spaceCode == null) return null;
final spaceId = await joinSpaceWithCode(
context,
spaceCode,
);
await _spaceStorage.remove(
PLocalKey.cachedSpaceCodeToJoin,
);
if (spaceId != null) {
context.go('/rooms/spaces/$spaceId/details');
return spaceId;
}
return null;
}
Future<String?> joinSpaceWithCode(
BuildContext context,
String spaceCode, {
String? notFoundError,
}) async {
final client = _pangeaController.matrixState.client;
await _spaceStorage.write(
PLocalKey.justInputtedCode,
spaceCode,
);
final resp = await showFutureLoadingDialog<KnockSpaceResponse>(
context: context,
future: () async {
final KnockSpaceResponse knockResult =
await client.knockWithCode(spaceCode);
if (knockResult.roomIds.isEmpty &&
knockResult.alreadyJoined.isEmpty &&
!knockResult.rateLimited) {
throw notFoundError ?? L10n.of(context).unableToFindRoom;
}
return knockResult;
},
);
if (resp.isError || resp.result == null) {
return null;
}
if (resp.result!.rateLimited) {
await showDialog(
context: context,
builder: (context) => const TooManyRequestsDialog(),
);
return null;
}
String? roomIdToJoin = resp.result!.roomIds.firstOrNull;
final alreadyJoined = resp.result!.alreadyJoined;
if (alreadyJoined.isNotEmpty) {
final room = client.getRoomById(alreadyJoined.first);
if (room?.membership == Membership.join) {
return alreadyJoined.first;
} else if (room != null) {
roomIdToJoin = alreadyJoined.first;
}
}
if (roomIdToJoin == null) {
return null;
}
await showFutureLoadingDialog(
context: context,
future: () => _joinSpace(roomIdToJoin!),
);
if (resp.isError) {
return null;
}
return roomIdToJoin;
}
Future<void> _joinSpace(String spaceId) async {
final client = _pangeaController.matrixState.client;
await client.joinRoomById(spaceId);
Room? room = client.getRoomById(spaceId);
if (room == null) {
await client.waitForRoomInSync(
spaceId,
join: true,
);
room = client.getRoomById(spaceId);
if (room == null) {
throw Exception("Failed to join space with id $spaceId");
}
}
if (room.membership != Membership.join) {
await room.client.waitForRoomInSync(room.id, join: true);
}
// Sometimes, the invite event comes through after the join event and
// replaces it, so membership gets out of sync. In this case,
// load the true value from the server.
// Related github issue: https://github.com/pangeachat/client/issues/2098
if (room.membership !=
room
.getParticipants()
.firstWhereOrNull((u) => u.id == room?.client.userID)
?.membership) {
await room.requestParticipants();
}
}
}

View file

@ -1,277 +0,0 @@
// ignore_for_file: implementation_imports
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:async/src/result/result.dart' as result;
import 'package:collection/collection.dart';
import 'package:get_storage/get_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../bot/widgets/bot_face_svg.dart';
import '../../common/controllers/base_controller.dart';
class ClassController extends BaseController {
late PangeaController _pangeaController;
static final GetStorage _classStorage = GetStorage('class_storage');
ClassController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
Future<void> cacheSpaceCode(String code) async {
if (code.isEmpty) return;
await _classStorage.write(
PLocalKey.cachedClassCodeToJoin,
code,
);
}
String? justInputtedCode() {
return _classStorage.read(PLocalKey.justInputtedCode);
}
String? get cachedClassCode {
return _classStorage.read(PLocalKey.cachedClassCodeToJoin);
}
String? get cachedAlias {
return _classStorage.read(PLocalKey.cachedAliasToJoin);
}
Future<void> joinCachedSpaceCode(BuildContext context) async {
final String? classCode = cachedClassCode;
final String? alias = cachedAlias;
if (classCode != null) {
await joinClasswithCode(
context,
classCode,
);
await _classStorage.remove(
PLocalKey.cachedClassCodeToJoin,
);
} else if (alias != null) {
await joinCachedRoomAlias(alias, context);
await _classStorage.remove(PLocalKey.cachedAliasToJoin);
}
}
Future<void> joinCachedRoomAlias(
String alias,
BuildContext context,
) async {
if (alias.isEmpty) {
context.go("/rooms");
return;
}
final client = Matrix.of(context).client;
if (!client.isLogged()) {
await _classStorage.write(PLocalKey.cachedAliasToJoin, alias);
context.go("/home");
return;
}
Room? room = client.getRoomByAlias(alias) ?? client.getRoomById(alias);
if (room != null) {
room.isSpace
? context.go("/rooms/spaces/${room.id}/details")
: context.go("/rooms/${room.id}");
return;
}
final roomID = await client.joinRoom(alias);
room = client.getRoomById(roomID);
if (room == null) {
await client.waitForRoomInSync(roomID);
room = client.getRoomById(roomID);
if (room == null) {
context.go("/rooms");
return;
}
}
room.isSpace
? context.go("/rooms/spaces/${room.id}/details")
: context.go("/rooms/${room.id}");
}
Future<result.Result<String?>> joinClasswithCode(
BuildContext context,
String classCode, {
String? notFoundError,
}) async {
final client = Matrix.of(context).client;
final spaceID = await showFutureLoadingDialog<String?>(
context: context,
future: () async {
await _classStorage.write(
PLocalKey.justInputtedCode,
classCode,
);
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) {
return "429";
}
if (knockResponse.statusCode != 200) {
throw notFoundError ?? L10n.of(context).unableToFindRoom;
}
final knockResult = jsonDecode(knockResponse.body);
final foundClasses = List<String>.from(knockResult['rooms']);
final alreadyJoined = List<String>.from(knockResult['already_joined']);
final bool inFoundClass = foundClasses.isNotEmpty &&
_pangeaController.matrixState.client.rooms.any(
(room) => room.id == foundClasses.first,
);
if (alreadyJoined.isNotEmpty || inFoundClass) {
final room = client.getRoomById(alreadyJoined.first);
if (!(room?.isSpace ?? true)) {
context.go("/rooms/${alreadyJoined.first}");
} else {
context.go("/rooms/spaces/${alreadyJoined.first}/details");
}
return null;
}
if (foundClasses.isEmpty) {
throw notFoundError ?? L10n.of(context).unableToFindRoom;
}
final chosenClassId = foundClasses.first;
return chosenClassId;
},
);
if (spaceID.isError || spaceID.result == null) {
return spaceID;
}
if (spaceID.result == "429") {
await _showTooManyRequestsPopup(context);
return result.Result.error(
Exception(L10n.of(context).tooManyRequestsWarning),
StackTrace.current,
);
}
try {
await client.joinRoomById(spaceID.result!);
Room? room = client.getRoomById(spaceID.result!);
if (room == null) {
await _pangeaController.matrixState.client.waitForRoomInSync(
spaceID.result!,
join: true,
);
room = client.getRoomById(spaceID.result!);
if (room == null) return spaceID;
}
GoogleAnalytics.joinClass(classCode);
if (room.membership != Membership.join) {
await room.client.waitForRoomInSync(room.id, join: true);
}
// Sometimes, the invite event comes through after the join event and
// replaces it, so membership gets out of sync. In this case,
// load the true value from the server.
// Related github issue: https://github.com/pangeachat/client/issues/2098
if (room.membership !=
room
.getParticipants()
.firstWhereOrNull((u) => u.id == room?.client.userID)
?.membership) {
await room.requestParticipants();
}
if (room.isSpace) {
context.go("/rooms/spaces/${room.id}/details");
} else {
context.go("/rooms/${room.id}");
}
return spaceID;
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"classCode": classCode,
},
);
return result.Result.error(e, s);
}
}
Future<void> _showTooManyRequestsPopup(BuildContext context) async {
await showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const BotFace(
width: 100,
expression: BotExpression.idle,
),
const SizedBox(height: 16),
Text(
// "Are you like me?",
L10n.of(context).areYouLikeMe,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
// "Too many attempts made. Please try again in 5 minutes.",
L10n.of(context).tryAgainLater,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Close"),
),
],
),
),
);
},
);
}
}

View file

@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/layouts/empty_page.dart';
import 'package:fluffychat/widgets/matrix.dart';
class JoinWithAlias extends StatefulWidget {
final String? alias;
const JoinWithAlias({super.key, this.alias});
@override
State<JoinWithAlias> createState() => _JoinWithAliasState();
}
class _JoinWithAliasState extends State<JoinWithAlias> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) => showFutureLoadingDialog(
context: context,
future: () async => _joinRoom(),
),
);
}
Future<void> _joinRoom() async {
if (widget.alias == null || widget.alias!.isEmpty) {
context.go("/rooms");
return;
}
await MatrixState.pangeaController.classController.joinCachedRoomAlias(
widget.alias!,
context,
);
}
@override
Widget build(BuildContext context) => const EmptyPage();
}

View file

@ -33,7 +33,7 @@ class _JoinClassWithLinkState extends State<JoinClassWithLink> {
}
if (widget.classCode != null) {
await MatrixState.pangeaController.classController
await MatrixState.pangeaController.spaceCodeController
.cacheSpaceCode(widget.classCode!);
}
context.go("/home");

View file

@ -0,0 +1,69 @@
import 'dart:convert';
import 'package:http/http.dart' hide Client;
import 'package:matrix/matrix.dart';
import 'package:matrix/matrix_api_lite/generated/api.dart';
extension on Api {
Future<KnockSpaceResponse> knockSpace(String code) async {
final requestUri = Uri(
path: '_synapse/client/pangea/v1/knock_with_code',
);
final request = Request('POST', baseUri!.resolveUri(requestUri));
request.headers['content-type'] = 'application/json';
request.headers['authorization'] = 'Bearer ${bearerToken!}';
request.bodyBytes = utf8.encode(
jsonEncode({
'access_code': code,
}),
);
final response = await httpClient.send(request);
if (response.statusCode != 200) {
if (response.statusCode == 429) {
return KnockSpaceResponse(
roomIds: [],
alreadyJoined: [],
rateLimited: true,
);
}
throw response;
}
final responseBody = await response.stream.toBytes();
final responseString = utf8.decode(responseBody);
final json = jsonDecode(responseString);
return KnockSpaceResponse.fromJson(json);
}
}
extension KnockSpaceExtension on Client {
Future<KnockSpaceResponse> knockWithCode(String code) => knockSpace(code);
}
class KnockSpaceResponse {
final List<String> roomIds;
final List<String> alreadyJoined;
final bool rateLimited;
KnockSpaceResponse({
required this.roomIds,
required this.alreadyJoined,
required this.rateLimited,
});
factory KnockSpaceResponse.fromJson(Map<String, dynamic> json) {
return KnockSpaceResponse(
roomIds: List<String>.from(json['rooms'] ?? []),
alreadyJoined: List<String>.from(json['already_joined'] ?? []),
rateLimited: false,
);
}
Map<String, dynamic> toJson() {
return {
'rooms': roomIds,
'already_joined': alreadyJoined,
'rate_limited': rateLimited,
};
}
}

View file

@ -1,21 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class SpaceCodeUtil {
static const codeLength = 7;
static bool isValidCode(String? spacecode) {
if (spacecode == null) return false;
return spacecode.length == codeLength && spacecode.contains(r'[0-9]');
}
static Future<String> generateSpaceCode(Client client) async {
final response = await client.httpClient.get(
Uri.parse(
@ -36,41 +23,4 @@ class SpaceCodeUtil {
throw Exception('Invalid response, access_code not found $response');
}
}
static Future<void> joinWithSpaceCodeDialog(
BuildContext context,
) async {
final String? spaceCode = await showTextInputDialog(
context: context,
title: L10n.of(context).joinWithClassCode,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).joinWithClassCodeHint,
autoSubmit: true,
);
if (spaceCode == null || spaceCode.isEmpty) return;
await MatrixState.pangeaController.classController.joinClasswithCode(
context,
spaceCode,
);
}
static messageDialog(
BuildContext context,
String title,
void Function()? action,
) =>
showDialog(
context: context,
useRootNavigator: false,
builder: (context) => AlertDialog(
content: Text(title),
actions: [
TextButton(
onPressed: action,
child: Text(L10n.of(context).ok),
),
],
),
);
}

View file

@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
class SpaceFloatingActionButtons extends StatelessWidget {
const SpaceFloatingActionButtons({super.key});
@override
Widget build(BuildContext context) {
return IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
FloatingActionButton.extended(
onPressed: () => SpaceCodeUtil.joinWithSpaceCodeDialog(context),
icon: const Icon(Icons.join_right_outlined),
label: Text(
L10n.of(context).join,
overflow: TextOverflow.fade,
),
),
FloatingActionButton.extended(
onPressed: () => context.go('/rooms/newspace'),
icon: const Icon(Icons.add),
label: Text(
L10n.of(context).course,
overflow: TextOverflow.fade,
),
),
],
),
);
}
}

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
class TooManyRequestsDialog extends StatelessWidget {
const TooManyRequestsDialog({super.key});
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const BotFace(
width: 100,
expression: BotExpression.idle,
),
const SizedBox(height: 16),
Text(
L10n.of(context).areYouLikeMe,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
L10n.of(context).tryAgainLater,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(L10n.of(context).close),
),
],
),
),
);
}
}