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,247 @@
// 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/app_config.dart';
import 'package:tatchat/config/themes.dart';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/pages/sign_in/view_model/model/public_homeserver_data.dart';
import 'package:tatchat/pages/sign_in/view_model/sign_in_view_model.dart';
import 'package:tatchat/utils/localized_exception_extension.dart';
import 'package:tatchat/utils/sign_in_flows/check_homeserver.dart';
import 'package:tatchat/widgets/layouts/login_scaffold.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:tatchat/widgets/view_model_builder.dart';
import 'package:material_ui/material_ui.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignInPage extends StatelessWidget {
final bool signUp;
const SignInPage({required this.signUp, super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ViewModelBuilder(
create: () => SignInViewModel(Matrix.of(context), signUp: signUp),
builder: (context, viewModel, _) {
final state = viewModel.value;
final publicHomeservers = state.filteredPublicHomeservers;
final selectedHomserver = state.selectedHomeserver;
return LoginScaffold(
appBar: AppBar(
leading:
state.loginLoading.connectionState == ConnectionState.waiting
? CloseButton(
onPressed: () =>
viewModel.setLoginLoading(AsyncSnapshot.nothing()),
)
: BackButton(onPressed: Navigator.of(context).pop),
backgroundColor: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surface,
scrolledUnderElevation: 0,
centerTitle: true,
title: Text(
signUp
? L10n.of(context).createNewAccount
: L10n.of(context).login,
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
spacing: 16,
children: [
SelectableText(
signUp
? L10n.of(context).signUpGreeting
: L10n.of(context).signInGreeting,
textAlign: .center,
),
TextField(
readOnly:
state.publicHomeservers.connectionState ==
ConnectionState.waiting ||
state.loginLoading.connectionState ==
ConnectionState.waiting,
controller: viewModel.filterTextController,
autocorrect: false,
keyboardType: TextInputType.url,
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
errorText: state.publicHomeservers.error?.toLocalizedString(
context,
),
prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context).searchOrEnterHomeserverAddress,
),
),
if (state.publicHomeservers.connectionState ==
ConnectionState.done)
Expanded(
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
color: theme.colorScheme.surfaceContainerLow,
child: RadioGroup<PublicHomeserverData>(
groupValue: state.selectedHomeserver,
onChanged: viewModel.selectHomeserver,
child: ListView.builder(
itemCount: publicHomeservers.length,
itemBuilder: (context, i) {
final server = publicHomeservers[i];
final website = server.website;
return RadioListTile(
value: server,
enabled:
state.loginLoading.connectionState !=
ConnectionState.waiting,
title: Row(
children: [
Expanded(
child: Text(server.name ?? 'Unknown'),
),
if (website != null)
SizedBox.square(
dimension: 32,
child: IconButton(
tooltip: website,
icon: const Icon(
Icons.open_in_new_outlined,
size: 16,
),
onPressed: () =>
launchUrlString(website),
),
),
],
),
subtitle: Column(
spacing: 4.0,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (server.features?.isNotEmpty == true)
Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: [
...?server.languages?.map(
(language) => Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
color: theme
.colorScheme
.tertiaryContainer,
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 3.0,
),
child: Text(
language,
style: TextStyle(
fontSize: 10,
color: theme
.colorScheme
.onTertiaryContainer,
),
),
),
),
),
...server.features!.map(
(feature) => Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
color: theme
.colorScheme
.secondaryContainer,
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 3.0,
),
child: Text(
feature,
style: TextStyle(
fontSize: 10,
color: theme
.colorScheme
.onSecondaryContainer,
),
),
),
),
),
],
),
Text(
server.description ?? 'A matrix homeserver',
),
],
),
);
},
),
),
),
)
else
Center(child: CircularProgressIndicator.adaptive()),
],
),
),
bottomNavigationBar: AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child:
selectedHomserver == null ||
!publicHomeservers.contains(selectedHomserver)
? const SizedBox.shrink()
: Material(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SafeArea(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
),
onPressed:
state.loginLoading.connectionState ==
ConnectionState.waiting
? null
: () => connectToHomeserverFlow(
selectedHomserver,
context,
viewModel.setLoginLoading,
signUp,
),
child:
state.loginLoading.connectionState ==
ConnectionState.waiting
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context).continueText),
),
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
class PublicHomeserverData {
final String? name;
final String? clientDomain;
final String? website;
final String? isp;
final String? staffJur;
final String? rules;
final String? privacy;
final bool? usingVanillaReg;
final String? description;
final String? regMethod;
final String? regLink;
final String? software;
final String? version;
final bool? captcha;
final bool? email;
final List<String>? languages;
final List<String>? features;
final int? onlineStatus;
final String? serverDomain;
final int? verStatus;
final int? roomDirectory;
final bool? slidingSync;
final bool? ipv6;
PublicHomeserverData({
this.name,
this.clientDomain,
this.website,
this.isp,
this.staffJur,
this.rules,
this.privacy,
this.usingVanillaReg,
this.description,
this.regMethod,
this.regLink,
this.software,
this.version,
this.captcha,
this.email,
this.languages,
this.features,
this.onlineStatus,
this.serverDomain,
this.verStatus,
this.roomDirectory,
this.slidingSync,
this.ipv6,
});
factory PublicHomeserverData.fromJson(Map<String, dynamic> json) {
return PublicHomeserverData(
name: json['name'],
clientDomain: json['client_domain'],
website: json['website'],
isp: json['isp'],
staffJur: json['staff_jur'],
rules: json['rules'],
privacy: json['privacy'],
usingVanillaReg: json['using_vanilla_reg'],
description: json['description'],
regMethod: json['reg_method'],
regLink: json['reg_link'],
software: json['software'],
version: json['version'],
captcha: json['captcha'],
email: json['email'],
languages: List<String>.from(json['languages'] ?? []),
features: List<String>.from(json['features'] ?? []),
onlineStatus: json['online_status'],
serverDomain: json['server_domain'],
verStatus: json['ver_status'],
roomDirectory: json['room_directory'],
slidingSync: json['sliding_sync'],
ipv6: json['ipv6'],
);
}
}

View File

@@ -0,0 +1,15 @@
// 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/pages/sign_in/view_model/model/public_homeserver_data.dart';
import 'package:material_ui/material_ui.dart';
class SignInState {
PublicHomeserverData? selectedHomeserver;
AsyncSnapshot<List<PublicHomeserverData>> publicHomeservers =
const AsyncSnapshot.nothing();
List<PublicHomeserverData> filteredPublicHomeservers = [];
AsyncSnapshot<bool> loginLoading = const AsyncSnapshot.nothing();
}

View File

@@ -0,0 +1,114 @@
// 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:collection/collection.dart';
import 'package:tatchat/config/app_config.dart';
import 'package:tatchat/config/setting_keys.dart';
import 'package:tatchat/pages/sign_in/view_model/model/public_homeserver_data.dart';
import 'package:tatchat/pages/sign_in/view_model/sign_in_state.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:flutter/widgets.dart';
import 'package:matrix/matrix_api_lite/utils/logs.dart';
class SignInViewModel extends ValueNotifier<SignInState> {
final MatrixState matrixService;
final bool signUp;
final TextEditingController filterTextController = TextEditingController();
SignInViewModel(this.matrixService, {required this.signUp})
: super(SignInState()) {
refreshPublicHomeservers();
filterTextController.addListener(_filterHomeservers);
}
@override
void dispose() {
filterTextController.removeListener(_filterHomeservers);
filterTextController.dispose();
super.dispose();
}
void _filterHomeservers() {
final filterText = filterTextController.text.trim().toLowerCase();
final filteredPublicHomeservers =
value.publicHomeservers.data
?.where(
(homeserver) =>
homeserver.name?.toLowerCase().contains(filterText) ?? false,
)
.toList() ??
[];
if (filterText.length >= 3 &&
(filterText.contains('.') || filterText.endsWith('localhost')) &&
Uri.tryParse(filterText) != null &&
!filteredPublicHomeservers.any(
(homeserver) => homeserver.name == filterText,
)) {
filteredPublicHomeservers.add(PublicHomeserverData(name: filterText));
}
value.filteredPublicHomeservers = filteredPublicHomeservers;
notifyListeners();
}
Future<void> refreshPublicHomeservers() async {
notifyListeners();
value.publicHomeservers = AsyncSnapshot.waiting();
final defaultHomeserverData = PublicHomeserverData(
name: AppSettings.defaultHomeserver.value,
);
try {
final client = await matrixService.getLoginClient();
final response = await client.httpClient.get(AppConfig.homeserverList);
final json = jsonDecode(response.body) as Map<String, dynamic>;
final homeserverJsonList = json['public_servers'] as List;
final publicHomeservers = homeserverJsonList
.map((json) => PublicHomeserverData.fromJson(json))
.toList();
if (signUp) {
publicHomeservers.removeWhere((server) {
return server.regMethod == null;
});
}
final defaultServer = publicHomeservers.singleWhereOrNull(
(server) => server.name == AppSettings.defaultHomeserver.value,
);
if (defaultServer == null) {
publicHomeservers.insert(0, defaultHomeserverData);
}
value.selectedHomeserver =
value.selectedHomeserver ?? publicHomeservers.first;
value.publicHomeservers = AsyncSnapshot.withData(
ConnectionState.done,
publicHomeservers,
);
notifyListeners();
} catch (e, s) {
Logs().w('Unable to fetch public homeservers...', e, s);
value.selectedHomeserver = defaultHomeserverData;
value.publicHomeservers = AsyncSnapshot.withData(ConnectionState.done, [
defaultHomeserverData,
]);
notifyListeners();
}
_filterHomeservers();
}
void selectHomeserver(PublicHomeserverData? publicHomeserverData) {
value.selectedHomeserver = publicHomeserverData;
notifyListeners();
}
void setLoginLoading(AsyncSnapshot<bool> loginLoading) {
value.loginLoading = loginLoading;
notifyListeners();
}
}