feat: use cached space code to join space on create account (#4224)
This commit is contained in:
parent
343a27e80a
commit
828dbab9a8
20 changed files with 308 additions and 675 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
160
lib/pangea/spaces/controllers/space_code_controller.dart
Normal file
160
lib/pangea/spaces/controllers/space_code_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
69
lib/pangea/spaces/utils/knock_space_extension.dart
Normal file
69
lib/pangea/spaces/utils/knock_space_extension.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/pangea/spaces/widgets/too_many_requests_dialog.dart
Normal file
48
lib/pangea/spaces/widgets/too_many_requests_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue