Initial commit
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2026-09-11 15:59:56 -03:00
commit 0037b0b9cf
651 changed files with 199705 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:tatchat/utils/file_selector.dart';
import 'package:tatchat/widgets/future_loading_dialog.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:go_router/go_router.dart';
import 'package:material_ui/material_ui.dart';
Future<void> restoreBackupFlow(BuildContext context) async {
final matrix = Matrix.of(context);
final picked = await selectFiles(context);
final file = picked.firstOrNull;
if (file == null) return;
if (!context.mounted) return;
final result = await showFutureLoadingDialog(
context: context,
future: () async {
final client = await matrix.getLoginClient();
await client.database.importDump(
String.fromCharCodes(await file.readAsBytes()),
);
await client.init(
waitForFirstSync: false,
waitUntilLoadCompletedLoaded: false,
);
matrix.initMatrix();
return client.isLogged();
},
);
if (result.result == true && context.mounted) {
context.go('/rooms');
}
}

View File

@@ -0,0 +1,199 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:tatchat/config/setting_keys.dart';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/pages/intro/flows/restore_backup_flow.dart';
import 'package:tatchat/utils/platform_infos.dart';
import 'package:tatchat/widgets/layouts/login_scaffold.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:go_router/go_router.dart';
import 'package:material_ui/material_ui.dart';
import 'package:url_launcher/url_launcher_string.dart';
class IntroPage extends StatelessWidget {
final bool isLoading, hasPresetHomeserver;
final String? loggingInToHomeserver, welcomeText;
final VoidCallback login, register;
const IntroPage({
required this.isLoading,
required this.loggingInToHomeserver,
super.key,
required this.hasPresetHomeserver,
required this.welcomeText,
required this.login,
required this.register,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final addMultiAccount = Matrix.of(
context,
).widget.clients.any((client) => client.isLogged());
final loggingInToHomeserver = this.loggingInToHomeserver;
return LoginScaffold(
appBar: AppBar(
centerTitle: true,
title: addMultiAccount
? Text(L10n.of(context).addAccount)
: Text(AppSettings.applicationName.value),
actions: [
PopupMenuButton(
useRootNavigator: true,
itemBuilder: (_) => [
PopupMenuItem(
onTap: isLoading ? null : () => restoreBackupFlow(context),
child: Row(
mainAxisSize: .min,
spacing: 12,
children: [
const Icon(Icons.import_export_outlined),
Expanded(
child: Text(
L10n.of(context).hydrate,
maxLines: 1,
overflow: .ellipsis,
),
),
],
),
),
PopupMenuItem(
onTap: () => launchUrlString(AppSettings.privacyPolicy.value),
child: Row(
mainAxisSize: .min,
spacing: 12,
children: [
const Icon(Icons.privacy_tip_outlined),
Text(L10n.of(context).privacy),
],
),
),
PopupMenuItem(
onTap: () => PlatformInfos.showDialog(context),
child: Row(
mainAxisSize: .min,
spacing: 12,
children: [
const Icon(Icons.info_outlined),
Text(L10n.of(context).about),
],
),
),
],
),
],
),
body: isLoading
? Center(
child: Column(
mainAxisAlignment: .center,
children: [
CircularProgressIndicator.adaptive(),
if (loggingInToHomeserver != null)
Text(L10n.of(context).logInTo(loggingInToHomeserver)),
],
),
)
: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
children: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(32.0),
child: Hero(
tag: 'info-logo',
child: ClipRRect(
borderRadius: BorderRadius.circular(128),
child: Image.asset(
'./assets/logo/mini/logo_mini.png',
width: 128,
height: 128,
),
),
),
),
Text(
L10n.of(context).appSubtitle,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
welcomeText ?? L10n.of(context).appDescription,
textAlign: TextAlign.center,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .stretch,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.secondary,
foregroundColor:
theme.colorScheme.onSecondary,
),
onPressed: hasPresetHomeserver
? register
: () => context.go(
'${GoRouterState.of(context).uri.path}/sign_up',
),
child: Text(
L10n.of(context).createNewAccount,
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: login,
child: Text(L10n.of(context).signIn),
),
if (!hasPresetHomeserver)
TextButton(
onPressed: () async {
final client = await Matrix.of(
context,
).getLoginClient();
if (!context.mounted) return;
context.go(
'${GoRouterState.of(context).uri.path}/login',
extra: client,
);
},
child: Text(
L10n.of(context).loginWithMatrixId,
),
),
],
),
),
const SizedBox(height: 36),
],
),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,144 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'dart:convert';
import 'package:tatchat/config/setting_keys.dart';
import 'package:tatchat/pages/intro/intro_page.dart';
import 'package:tatchat/pages/sign_in/view_model/model/public_homeserver_data.dart';
import 'package:tatchat/utils/localized_exception_extension.dart';
import 'package:tatchat/utils/matrix_sdk_extensions/oidc_session_json_extension.dart';
import 'package:tatchat/utils/sign_in_flows/check_homeserver.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix_api_lite/utils/logs.dart';
import 'package:matrix/msc_extensions/msc_2964_oidc_login_flow/msc_2964_oidc_login_flow.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_html/universal_html.dart' as web;
class IntroPagePresenter extends StatefulWidget {
const IntroPagePresenter({super.key});
@override
State<IntroPagePresenter> createState() => _IntroPagePresenterState();
}
class _IntroPagePresenterState extends State<IntroPagePresenter> {
bool isLoading = kIsWeb;
String? loggingInToHomeserver;
@override
void initState() {
super.initState();
if (kIsWeb) _finishOidcLogin();
}
Future<void> _finishOidcLogin() async {
final store = await SharedPreferences.getInstance();
final storedHomeserverString = store.getString(
OidcSessionJsonExtension.homeserverStoreKey,
);
final homeserverUrl = storedHomeserverString == null
? null
: Uri.tryParse(storedHomeserverString);
final oidcSessionString = store.getString(
OidcSessionJsonExtension.storeKey,
);
final session = oidcSessionString == null
? null
: OidcSessionJsonExtension.fromJson(jsonDecode(oidcSessionString));
await store.remove(OidcSessionJsonExtension.storeKey);
await store.remove(OidcSessionJsonExtension.homeserverStoreKey);
if (!mounted) return;
if (homeserverUrl == null || session == null) {
setState(() {
isLoading = false;
});
return;
}
setState(() {
loggingInToHomeserver = homeserverUrl.origin;
});
try {
final returnUrl = Uri.parse(web.window.location.href);
final queryParameters = returnUrl.hasFragment
? Uri.parse(returnUrl.fragment).queryParameters
: returnUrl.queryParameters;
final code = queryParameters['code'] as String;
final state = queryParameters['state'] as String;
final client = await Matrix.of(context).getLoginClient();
await client.checkHomeserver(homeserverUrl);
await client.oidcLogin(session: session, code: code, state: state);
if (!mounted) return;
context.go('/backup');
} catch (e, s) {
Logs().w('Unable to login via OIDC', e, s);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
}
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
void _login() {
final presetHomeserver = AppSettings.presetHomeserver.value;
if (presetHomeserver.isEmpty) {
context.go('${GoRouterState.of(context).uri.path}/sign_in');
return;
}
connectToHomeserverFlow(
PublicHomeserverData(name: presetHomeserver),
context,
(snapshot) {},
false,
);
}
void _signUp() {
final presetHomeserver = AppSettings.presetHomeserver.value;
if (presetHomeserver.isEmpty) {
context.go('${GoRouterState.of(context).uri.path}/sign_up');
return;
}
connectToHomeserverFlow(
PublicHomeserverData(name: presetHomeserver),
context,
(snapshot) {},
true,
);
}
@override
Widget build(BuildContext context) {
return IntroPage(
isLoading: isLoading,
loggingInToHomeserver: loggingInToHomeserver,
hasPresetHomeserver: AppSettings.presetHomeserver.value.isNotEmpty,
welcomeText: AppSettings.welcomeText.value.isEmpty
? null
: AppSettings.welcomeText.value,
login: _login,
register: _signUp,
);
}
}