Merge branch 'braid/integration-tests' into 'main'

chore: add integration tests

See merge request famedly/fluffychat!1062
This commit is contained in:
The one with the Braid 2023-01-03 19:17:06 +00:00
commit 20e26b3747
18 changed files with 491 additions and 103 deletions

View File

@ -1,7 +1,9 @@
variables: variables:
FLUTTER_VERSION: 3.3.9 FLUTTER_VERSION: 3.3.9
image: cirrusci/flutter:${FLUTTER_VERSION} image:
name: cirrusci/flutter:${FLUTTER_VERSION}
pull_policy: if-not-present
.shared_windows_runners: .shared_windows_runners:
tags: tags:
@ -16,7 +18,7 @@ stages:
code_analyze: code_analyze:
stage: test stage: test
script: [./scripts/code_analyze.sh] script: [ ./scripts/code_analyze.sh ]
artifacts: artifacts:
reports: reports:
codequality: code-quality-report.json codequality: code-quality-report.json
@ -26,13 +28,13 @@ code_analyze:
widget_test: widget_test:
stage: test stage: test
script: [flutter test] script: [ flutter test ]
tags: tags:
- docker - docker
- famedly - famedly
# the basic integration test configuration testing FLOSS builds on Synapse # the basic integration test configuration testing FLOSS builds on Synapse
.integration_test: integration_test:
image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/integration/stable:${FLUTTER_VERSION} image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/integration/stable:${FLUTTER_VERSION}
stage: test stage: test
services: services:
@ -49,15 +51,13 @@ widget_test:
FF_NETWORK_PER_BUILD: "true" FF_NETWORK_PER_BUILD: "true"
# Tell docker CLI how to talk to Docker daemon. # Tell docker CLI how to talk to Docker daemon.
DOCKER_HOST: tcp://docker:2375/ DOCKER_HOST: tcp://docker:2375/
# Use the overlayfs driver for improved performance. # Use the btrfs driver for improved performance.
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: btrfs
# Disable TLS since we're running inside local network. # Disable TLS since we're running inside local network.
DOCKER_TLS_CERTDIR: "" DOCKER_TLS_CERTDIR: ""
HOMESERVER: "docker" HOMESERVER: docker
before_script: before_script:
# start AVD and keep running in background - scripts/integration-prepare-host.sh
- scripts/integration-start-avd.sh &
- scripts/integration-prepare-alpine.sh
# create test user environment variables # create test user environment variables
- source scripts/integration-create-environment-variables.sh - source scripts/integration-create-environment-variables.sh
# create Synapse instance # create Synapse instance
@ -65,31 +65,49 @@ widget_test:
# properly set the homeserver IP and create test users # properly set the homeserver IP and create test users
- scripts/integration-prepare-homeserver.sh - scripts/integration-prepare-homeserver.sh
script: script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- flutter pub get - flutter pub get
- flutter test integration_test - scrcpy --no-display --record video.mkv &
timeout: 20m - flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
after_script:
- ffmpeg -i video.mkv -vf scale=iw/2:-2 -crf 40 -b:v 2000k -preset fast video.mp4 || true
timeout: 30m
retry: 2
artifacts:
when: always
paths:
- video.mp4
tags: tags:
- docker - docker
- famedly - famedly
# integration tests for Linux builds # integration tests for Linux builds
### disabled because of Linux headless issues
.integration_test_linux: .integration_test_linux:
extends: .integration_test image: cirrusci/flutter:${FLUTTER_VERSION}
extends: integration_test
script: script:
- apk add cmake ninja gtk+3.0-dev clang pkgconf xz-dev libsecret-dev jsoncpp-dev - apt-get update
- apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libjsoncpp-dev
- flutter pub get - flutter pub get
- flutter test integration_test -d linux - flutter test integration_test -d linux --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
after_script: [ ]
artifacts:
# extending the default tests to test the Google-flavored builds # extending the default tests to test the Google-flavored builds
.integration_test_proprietary: integration_test_proprietary:
extends: .integration_test extends: integration_test
script: script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- git apply ./scripts/enable-android-google-services.patch - git apply ./scripts/enable-android-google-services.patch
- flutter pub get - flutter pub get
- flutter test integration_test - scrcpy --no-display --record video.mkv &
- flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
.release_mode_launches: release_mode_launches:
parallel: parallel:
matrix: matrix:
- FLAVOR: - FLAVOR:
@ -99,9 +117,9 @@ widget_test:
stage: test stage: test
before_script: before_script:
- | - |
if [ "$FLAVOR" == "proprietary" ]; then if [ "$FLAVOR" == "proprietary" ]; then
git apply ./scripts/enable-android-google-services.patch git apply ./scripts/enable-android-google-services.patch
fi fi
script: script:
# start AVD and keep running in background # start AVD and keep running in background
- scripts/integration-start-avd.sh & - scripts/integration-start-avd.sh &
@ -115,8 +133,8 @@ widget_test:
build_web: build_web:
stage: build stage: build
before_script: before_script:
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh] [ sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh ]
script: [./scripts/build-web.sh] script: [ ./scripts/build-web.sh ]
artifacts: artifacts:
paths: paths:
- build/web/ - build/web/
@ -166,7 +184,7 @@ build_windows:
build_android_debug: build_android_debug:
stage: build stage: build
script: [./scripts/build-android-debug.sh] script: [ ./scripts/build-android-debug.sh ]
artifacts: artifacts:
when: on_success when: on_success
paths: paths:
@ -183,7 +201,7 @@ build_android_apk:
before_script: before_script:
- git apply ./scripts/enable-android-google-services.patch - git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh - ./scripts/prepare-android-release.sh
script: [./scripts/build-android-apk.sh] script: [ ./scripts/build-android-apk.sh ]
artifacts: artifacts:
when: on_success when: on_success
paths: paths:
@ -200,7 +218,7 @@ deploy_playstore_internal:
before_script: before_script:
- git apply ./scripts/enable-android-google-services.patch - git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh - ./scripts/prepare-android-release.sh
script: [./scripts/release-playstore-beta.sh] script: [ ./scripts/release-playstore-beta.sh ]
artifacts: artifacts:
when: on_success when: on_success
paths: paths:
@ -267,7 +285,7 @@ build_linux_x86:
[ [
sudo apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install keyboard-configuration -y && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y, sudo apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install keyboard-configuration -y && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y,
] ]
script: [./scripts/build-linux.sh] script: [ ./scripts/build-linux.sh ]
tags: tags:
- docker - docker
- famedly - famedly
@ -278,9 +296,9 @@ build_linux_x86:
build_linux_arm64: build_linux_arm64:
stage: build stage: build
before_script: [flutter upgrade] before_script: [ flutter upgrade ]
script: [./scripts/build-linux.sh] script: [ ./scripts/build-linux.sh ]
tags: [docker_arm64] tags: [ docker_arm64 ]
only: only:
- main - main
- tags - tags
@ -292,7 +310,7 @@ build_linux_arm64:
update_dependencies: update_dependencies:
stage: build stage: build
needs: [] needs: [ ]
tags: tags:
- docker - docker
only: only:
@ -374,7 +392,7 @@ deploy_playstore:
before_script: before_script:
- git apply ./scripts/enable-android-google-services.patch - git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh - ./scripts/prepare-android-release.sh
script: [./scripts/release-playstore.sh] script: [ ./scripts/release-playstore.sh ]
resource_group: playstore_release resource_group: playstore_release
only: only:
- tags - tags

View File

@ -1,49 +1,117 @@
import 'dart:developer'; import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/pages/chat/chat_view.dart';
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:fluffychat/main.dart' as app; import 'package:fluffychat/main.dart' as app;
import 'package:shared_preferences/shared_preferences.dart';
import 'extensions/default_flows.dart';
import 'extensions/wait_for.dart';
import 'users.dart'; import 'users.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Integration Test', () { group(
testWidgets('Test if the app starts', (WidgetTester tester) async { 'Integration Test',
app.main(); () {
await tester.pumpAndSettle(); setUpAll(
() async {
// this random dialog popping up is super hard to cover in tests
SharedPreferences.setMockInitialValues({
SettingKeys.showNoGoogle: false,
});
try {
Hive.deleteFromDisk();
Hive.initFlutter();
} catch (_) {}
},
);
await Future.delayed(const Duration(seconds: 10)); testWidgets(
'Start app, login and logout',
(WidgetTester tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
await tester.ensureLoggedOut();
},
);
await tester.pumpAndSettle(); testWidgets(
'Login again',
(WidgetTester tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
},
);
expect(find.text('Connect'), findsOneWidget); testWidgets(
'Start chat and send message',
(WidgetTester tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
await tester.waitFor(find.byType(TextField));
await tester.enterText(find.byType(TextField), Users.user2.name);
await tester.pumpAndSettle();
final input = find.byType(TextField); await tester.scrollUntilVisible(
find.text('Chats'),
500,
scrollable: find.descendant(
of: find.byType(ChatListViewBody),
matching: find.byType(Scrollable),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Chats'));
await tester.pumpAndSettle();
await tester.waitFor(find.byType(SearchTitle));
await tester.pumpAndSettle();
expect(input, findsOneWidget); await tester.scrollUntilVisible(
find.text(Users.user2.name).first,
500,
scrollable: find.descendant(
of: find.byType(ChatListViewBody),
matching: find.byType(Scrollable),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text(Users.user2.name).first);
await tester.enterText(input, homeserver); try {
await tester.testTextInput.receiveAction(TextInputAction.done); await tester.waitFor(
await tester.pumpAndSettle(); find.byType(ChatView),
timeout: const Duration(seconds: 5),
);
} catch (_) {
// in case the homeserver sends the username as search result
if (find.byIcon(Icons.send_outlined).evaluate().isNotEmpty) {
await tester.tap(find.byIcon(Icons.send_outlined));
await tester.pumpAndSettle();
}
}
// in case registration is allowed await tester.waitFor(find.byType(ChatView));
try { await tester.enterText(find.byType(TextField).last, 'Test');
await tester.tap(find.text('Login')); await tester.pumpAndSettle();
await tester.pumpAndSettle(); try {
} catch (e) { await tester.waitFor(find.byIcon(Icons.send_outlined));
log('Registration is not allowed. Proceeding with login...'); await tester.tap(find.byIcon(Icons.send_outlined));
} } catch (_) {
await tester.pumpAndSettle(); await tester.testTextInput.receiveAction(TextInputAction.done);
}
final inputs = find.byType(TextField); await tester.pumpAndSettle();
await tester.waitFor(find.text('Test'));
await tester.enterText(inputs.first, Users.user1.name); await tester.pumpAndSettle();
await tester.enterText(inputs.last, Users.user1.password); },
await tester.testTextInput.receiveAction(TextInputAction.done); );
}); },
}); );
} }

View File

@ -0,0 +1,182 @@
import 'dart:developer';
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
import 'package:fluffychat/pages/settings_account/settings_account_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../users.dart';
import 'wait_for.dart';
extension DefaultFlowExtensions on WidgetTester {
Future<void> login() async {
final tester = this;
await tester.pumpAndSettle();
await tester.waitFor(find.text('Let\'s start'));
expect(find.text('Let\'s start'), findsOneWidget);
final input = find.byType(TextField);
expect(input, findsOneWidget);
// getting the placeholder in place
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
await tester.enterText(input, homeserver);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// in case registration is allowed
// try {
await Future.delayed(const Duration(milliseconds: 50));
await tester.scrollUntilVisible(
find.text('Login'),
500,
scrollable: find.descendant(
of: find.byKey(const Key('ConnectPageListView')),
matching: find.byType(Scrollable).first,
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
/*} catch (e) {
log('Registration is not allowed. Proceeding with login...');
}*/
await tester.pumpAndSettle();
await Future.delayed(const Duration(milliseconds: 50));
final inputs = find.byType(TextField);
await tester.enterText(inputs.first, Users.user1.name);
await tester.enterText(inputs.last, Users.user1.password);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
try {
// pumpAndSettle does not work in here as setState is called
// asynchronously
await tester.waitFor(
find.byType(LinearProgressIndicator),
timeout: const Duration(milliseconds: 1500),
skipPumpAndSettle: true,
);
} catch (_) {
// in case the input action does not work on the desired platform
if (find.text('Login').evaluate().isNotEmpty) {
await tester.tap(find.text('Login'));
}
}
try {
await tester.pumpAndSettle();
} catch (_) {
// may fail because of ongoing animation below dialog
}
await tester.waitFor(
find.byType(ChatListViewBody),
skipPumpAndSettle: true,
);
}
/// ensure PushProvider check passes
Future<void> acceptPushWarning() async {
final tester = this;
final matcher = find.maybeUppercaseText('Do not show again');
try {
await tester.waitFor(matcher, timeout: const Duration(seconds: 5));
// the FCM push error dialog to be handled...
await tester.tap(matcher);
await tester.pumpAndSettle();
} catch (_) {}
}
Future<void> ensureLoggedOut() async {
final tester = this;
await tester.pumpAndSettle();
if (find.byType(ChatListViewBody).evaluate().isNotEmpty) {
await tester.tap(find.byTooltip('Show menu'));
await tester.pumpAndSettle();
await tester.tap(find.text('Settings'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(
find.text('Account'),
500,
scrollable: find.descendant(
of: find.byKey(const Key('SettingsListViewContent')),
matching: find.byType(Scrollable),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Account'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(
find.text('Logout'),
500,
scrollable: find.descendant(
of: find.byType(SettingsAccountView),
matching: find.byType(Scrollable),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Logout'));
await tester.pumpAndSettle();
await tester.tap(find.maybeUppercaseText('Yes'));
await tester.pumpAndSettle();
}
}
Future<void> ensureAppStartedHomescreen({
Duration timeout = const Duration(seconds: 20),
}) async {
final tester = this;
await tester.pumpAndSettle();
final homeserverPickerFinder = find.byType(HomeserverPicker);
final chatListFinder = find.byType(ChatListViewBody);
final end = DateTime.now().add(timeout);
log(
'Waiting for HomeserverPicker or ChatListViewBody...',
name: 'Test Runner',
);
do {
if (DateTime.now().isAfter(end)) {
throw Exception(
'Timed out waiting for HomeserverPicker or ChatListViewBody');
}
await pumpAndSettle();
await Future.delayed(const Duration(milliseconds: 100));
} while (homeserverPickerFinder.evaluate().isEmpty &&
chatListFinder.evaluate().isEmpty);
if (homeserverPickerFinder.evaluate().isNotEmpty) {
log(
'Found HomeserverPicker, performing login.',
name: 'Test Runner',
);
await tester.login();
} else {
log(
'Found ChatListViewBody, skipping login.',
name: 'Test Runner',
);
}
await tester.acceptPushWarning();
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter_test/flutter_test.dart';
/// Workaround for https://github.com/flutter/flutter/issues/88765
extension WaitForExtension on WidgetTester {
Future<void> waitFor(
Finder finder, {
Duration timeout = const Duration(seconds: 20),
bool skipPumpAndSettle = false,
}) async {
final end = DateTime.now().add(timeout);
do {
if (DateTime.now().isAfter(end)) {
throw Exception('Timed out waiting for $finder');
}
if (!skipPumpAndSettle) {
await pumpAndSettle();
}
await Future.delayed(const Duration(milliseconds: 100));
} while (finder.evaluate().isEmpty);
}
}
extension MaybeUppercaseFinder on CommonFinders {
/// On Android some button labels are in uppercase while on iOS they
/// are not. This method tries both.
Finder maybeUppercaseText(
String text, {
bool findRichText = false,
bool skipOffstage = true,
}) {
try {
final finder = find.text(
text.toUpperCase(),
findRichText: findRichText,
skipOffstage: skipOffstage,
);
expect(finder, findsOneWidget);
return finder;
} catch (_) {
return find.text(
text,
findRichText: findRichText,
skipOffstage: skipOffstage,
);
}
}
}

View File

@ -1,15 +1,25 @@
import 'dart:io';
abstract class Users { abstract class Users {
const Users._(); const Users._();
static final user1 = User( static const user1 = User(
Platform.environment['USER1_NAME'] ?? 'alice', String.fromEnvironment(
Platform.environment['USER1_PW'] ?? 'AliceInWonderland', 'USER1_NAME',
defaultValue: 'alice',
),
String.fromEnvironment(
'USER1_PW',
defaultValue: 'AliceInWonderland',
),
); );
static final user2 = User( static const user2 = User(
Platform.environment['USER2_NAME'] ?? 'bob', String.fromEnvironment(
Platform.environment['USER2_PW'] ?? 'JoWirSchaffenDas', 'USER2_NAME',
defaultValue: 'bob',
),
String.fromEnvironment(
'USER2_PW',
defaultValue: 'JoWirSchaffenDas',
),
); );
} }
@ -20,5 +30,7 @@ class User {
const User(this.name, this.password); const User(this.name, this.password);
} }
final homeserver = const homeserver = 'http://${const String.fromEnvironment(
'http://${Platform.environment['HOMESERVER'] ?? 'localhost'}'; 'HOMESERVER',
defaultValue: 'localhost',
)}';

View File

@ -104,8 +104,13 @@ class ClientChooserButton extends StatelessWidget {
.map( .map(
(client) => PopupMenuItem( (client) => PopupMenuItem(
value: client, value: client,
child: FutureBuilder<Profile>( child: FutureBuilder<Profile?>(
future: client!.fetchOwnProfile(), // analyzer does not understand this type cast for error
// handling
//
// ignore: unnecessary_cast
future: (client!.fetchOwnProfile() as Future<Profile?>)
.onError((e, s) => null),
builder: (context, snapshot) => Row( builder: (context, snapshot) => Row(
children: [ children: [
Avatar( Avatar(

View File

@ -28,6 +28,7 @@ class ConnectPageView extends StatelessWidget {
), ),
), ),
body: ListView( body: ListView(
key: const Key('ConnectPageListView'),
children: [ children: [
if (Matrix.of(context).loginRegistrationSupported ?? false) ...[ if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
Padding( Padding(

View File

@ -8,6 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/platform_infos.dart'; import '../../utils/platform_infos.dart';
import 'login_view.dart'; import 'login_view.dart';
@ -30,7 +31,7 @@ class LoginController extends State<Login> {
void toggleShowPassword() => void toggleShowPassword() =>
setState(() => showPassword = !loading && !showPassword); setState(() => showPassword = !loading && !showPassword);
void login([_]) async { void login() async {
final matrix = Matrix.of(context); final matrix = Matrix.of(context);
if (usernameController.text.isEmpty) { if (usernameController.text.isEmpty) {
setState(() => usernameError = L10n.of(context)!.pleaseEnterYourUsername); setState(() => usernameError = L10n.of(context)!.pleaseEnterYourUsername);
@ -48,6 +49,9 @@ class LoginController extends State<Login> {
} }
setState(() => loading = true); setState(() => loading = true);
_coolDown?.cancel();
try { try {
final username = usernameController.text; final username = usernameController.text;
AuthenticationIdentifier identifier; AuthenticationIdentifier identifier;
@ -97,8 +101,8 @@ class LoginController extends State<Login> {
void _checkWellKnown(String userId) async { void _checkWellKnown(String userId) async {
if (mounted) setState(() => usernameError = null); if (mounted) setState(() => usernameError = null);
if (!userId.isValidMatrixId) return; if (!userId.isValidMatrixId) return;
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
try { try {
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
var newDomain = Uri.https(userId.domain!, ''); var newDomain = Uri.https(userId.domain!, '');
Matrix.of(context).getLoginClient().homeserver = newDomain; Matrix.of(context).getLoginClient().homeserver = newDomain;
DiscoveryInformation? wellKnownInformation; DiscoveryInformation? wellKnownInformation;
@ -112,14 +116,11 @@ class LoginController extends State<Login> {
// do nothing, newDomain is already set to a reasonable fallback // do nothing, newDomain is already set to a reasonable fallback
} }
if (newDomain != oldHomeserver) { if (newDomain != oldHomeserver) {
await showFutureLoadingDialog( Matrix.of(context)
context: context, .getLoginClient()
// do nothing if we error, we'll handle it below .checkHomeserver(newDomain)
future: () => Matrix.of(context) .catchError((e) {});
.getLoginClient()
.checkHomeserver(newDomain)
.catchError((e) {}),
);
if (Matrix.of(context).getLoginClient().homeserver == null) { if (Matrix.of(context).getLoginClient().homeserver == null) {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver; Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
// okay, the server we checked does not appear to be a matrix server // okay, the server we checked does not appear to be a matrix server
@ -140,15 +141,18 @@ class LoginController extends State<Login> {
return; return;
} }
} }
if (mounted) setState(() => usernameError = null); usernameError = null;
if (mounted) setState(() {});
} else { } else {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
if (mounted) { if (mounted) {
setState(() => setState(() {});
Matrix.of(context).getLoginClient().homeserver = oldHomeserver);
} }
} }
} catch (e) { } catch (e) {
if (mounted) setState(() => usernameError = e.toString()); Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
usernameError = e.toLocalizedString(context);
if (mounted) setState(() {});
} }
} }

View File

@ -60,7 +60,7 @@ class LoginView extends StatelessWidget {
controller: controller.passwordController, controller: controller.passwordController,
textInputAction: TextInputAction.go, textInputAction: TextInputAction.go,
obscureText: !controller.showPassword, obscureText: !controller.showPassword,
onSubmitted: controller.login, onSubmitted: (_) => controller.login(),
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outlined), prefixIcon: const Icon(Icons.lock_outlined),
errorText: controller.passwordError, errorText: controller.passwordError,
@ -87,9 +87,7 @@ class LoginView extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary, foregroundColor: Theme.of(context).colorScheme.onPrimary,
), ),
onPressed: controller.loading onPressed: controller.loading ? null : controller.login,
? null
: () => controller.login(context),
icon: const Icon(Icons.login_outlined), icon: const Icon(Icons.login_outlined),
label: controller.loading label: controller.loading
? const LinearProgressIndicator() ? const LinearProgressIndicator()

View File

@ -38,6 +38,7 @@ class SettingsView extends StatelessWidget {
body: ListTileTheme( body: ListTileTheme(
iconColor: Theme.of(context).colorScheme.onBackground, iconColor: Theme.of(context).colorScheme.onBackground,
child: ListView( child: ListView(
key: const Key('SettingsListViewContent'),
children: <Widget>[ children: <Widget>[
AnimatedContainer( AnimatedContainer(
height: controller.showChatBackupBanner ? 54 : 0, height: controller.showChatBackupBanner ? 54 : 0,

View File

@ -11,6 +11,7 @@ class ContentBanner extends StatelessWidget {
final void Function()? onEdit; final void Function()? onEdit;
final Client? client; final Client? client;
final double opacity; final double opacity;
final WidgetBuilder? placeholder;
const ContentBanner( const ContentBanner(
{this.mxContent, {this.mxContent,
@ -19,6 +20,7 @@ class ContentBanner extends StatelessWidget {
this.onEdit, this.onEdit,
this.client, this.client,
this.opacity = 0.75, this.opacity = 0.75,
this.placeholder,
Key? key}) Key? key})
: super(key: key); : super(key: key);
@ -54,6 +56,7 @@ class ContentBanner extends StatelessWidget {
uri: mxContent, uri: mxContent,
animated: true, animated: true,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: placeholder,
height: 400, height: 400,
width: 800, width: 800,
), ),

View File

@ -327,15 +327,15 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
); );
if (state != LoginState.loggedIn) { if (state != LoginState.loggedIn) {
widget.router!.currentState!.to( widget.router?.currentState?.to(
'/rooms', '/rooms',
queryParameters: widget.router!.currentState!.queryParameters, queryParameters: widget.router?.currentState?.queryParameters ?? {},
); );
} }
} else { } else {
widget.router!.currentState!.to( widget.router?.currentState?.to(
state == LoginState.loggedIn ? '/rooms' : '/home', state == LoginState.loggedIn ? '/rooms' : '/home',
queryParameters: widget.router!.currentState!.queryParameters, queryParameters: widget.router?.currentState?.queryParameters ?? {},
); );
} }
}); });

View File

@ -15,6 +15,7 @@ import '../utils/localized_exception_extension.dart';
class ProfileBottomSheet extends StatelessWidget { class ProfileBottomSheet extends StatelessWidget {
final String userId; final String userId;
final BuildContext outerContext; final BuildContext outerContext;
const ProfileBottomSheet({ const ProfileBottomSheet({
required this.userId, required this.userId,
required this.outerContext, required this.outerContext,
@ -78,6 +79,13 @@ class ProfileBottomSheet extends StatelessWidget {
mxContent: profile.avatarUrl, mxContent: profile.avatarUrl,
defaultIcon: Icons.account_circle_outlined, defaultIcon: Icons.account_circle_outlined,
client: Matrix.of(context).client, client: Matrix.of(context).client,
placeholder: (context) => Center(
child: Text(
userId.localpart ?? userId,
style:
Theme.of(context).textTheme.headline3,
),
),
), ),
), ),
ListTile( ListTile(

View File

@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
class ThemeBuilder extends StatefulWidget { class ThemeBuilder extends StatefulWidget {
final Widget Function( final Widget Function(
@ -34,6 +35,7 @@ class ThemeController extends State<ThemeBuilder> {
Color? _primaryColor; Color? _primaryColor;
ThemeMode get themeMode => _themeMode ?? ThemeMode.system; ThemeMode get themeMode => _themeMode ?? ThemeMode.system;
Color? get primaryColor => _primaryColor; Color? get primaryColor => _primaryColor;
static ThemeController of(BuildContext context) => static ThemeController of(BuildContext context) =>
@ -87,10 +89,18 @@ class ThemeController extends State<ThemeBuilder> {
super.initState(); super.initState();
} }
Color? get systemAccentColor { Color get systemAccentColor {
final color = SystemTheme.accentColor.accent; if (PlatformInfos.isLinux) return AppConfig.chatColor;
if (color == kDefaultSystemAccentColor) return AppConfig.chatColor; try {
return color; // a bad plugin implementation
// https://github.com/bdlukaa/system_theme/issues/10
final accentColor = SystemTheme.accentColor;
final color = accentColor.accent;
if (color == kDefaultSystemAccentColor) return AppConfig.chatColor;
return color;
} catch (_) {
return AppConfig.chatColor;
}
} }
@override @override

View File

@ -1,2 +0,0 @@
#!/usr/bin/env bash
apk update && apk add docker drill grep

View File

@ -33,3 +33,28 @@ echo "Homeserver is up."
curl -fS --retry 3 -XPOST -d "{\"username\":\"$USER1_NAME\", \"password\":\"$USER1_PW\", \"inhibit_login\":true, \"auth\": {\"type\":\"m.login.dummy\"}}" "http://$HOMESERVER/_matrix/client/r0/register" curl -fS --retry 3 -XPOST -d "{\"username\":\"$USER1_NAME\", \"password\":\"$USER1_PW\", \"inhibit_login\":true, \"auth\": {\"type\":\"m.login.dummy\"}}" "http://$HOMESERVER/_matrix/client/r0/register"
curl -fS --retry 3 -XPOST -d "{\"username\":\"$USER2_NAME\", \"password\":\"$USER2_PW\", \"inhibit_login\":true, \"auth\": {\"type\":\"m.login.dummy\"}}" "http://$HOMESERVER/_matrix/client/r0/register" curl -fS --retry 3 -XPOST -d "{\"username\":\"$USER2_NAME\", \"password\":\"$USER2_PW\", \"inhibit_login\":true, \"auth\": {\"type\":\"m.login.dummy\"}}" "http://$HOMESERVER/_matrix/client/r0/register"
usertoken1=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/login" -H "Content-Type: application/json" -d "{\"type\": \"m.login.password\", \"identifier\": {\"type\": \"m.id.user\",\"user\": \"$USER1_NAME\"},\"password\":\"$USER1_PW\"}" | jq -r '.access_token')
usertoken2=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/login" -H "Content-Type: application/json" -d "{\"type\": \"m.login.password\", \"identifier\": {\"type\": \"m.id.user\",\"user\": \"$USER2_NAME\"},\"password\":\"$USER2_PW\"}" | jq -r '.access_token')
# get usernames' mxids
mxid1=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/account/whoami" -H "Authorization: Bearer $usertoken1" | jq -r .user_id)
mxid2=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/account/whoami" -H "Authorization: Bearer $usertoken2" | jq -r .user_id)
# setting the display name to username
curl -fS --retry 3 -XPUT -d "{\"displayname\":\"$USER1_NAME\"}" "http://$HOMESERVER/_matrix/client/v3/profile/$mxid1/displayname" -H "Authorization: Bearer $usertoken1"
curl -fS --retry 3 -XPUT -d "{\"displayname\":\"$USER2_NAME\"}" "http://$HOMESERVER/_matrix/client/v3/profile/$mxid2/displayname" -H "Authorization: Bearer $usertoken2"
echo "Set display names"
# create new room to invite user too
roomID=$(curl --retry 3 --silent --fail -XPOST -d "{\"name\":\"$USER2_NAME\", \"is_direct\": true}" "http://$HOMESERVER/_matrix/client/r0/createRoom?access_token=$usertoken2" | jq -r '.room_id')
echo "Created room '$roomID'"
# send message in created room
curl --retry 3 --fail --silent -XPOST -d '{"msgtype":"m.text", "body":"joined room successfully"}' "http://$HOMESERVER/_matrix/client/r0/rooms/$roomID/send/m.room.message?access_token=$usertoken2"
echo "Sent message"
curl -fS --retry 3 -XPOST -d "{\"user_id\":\"$mxid1\"}" "http://$HOMESERVER/_matrix/client/r0/rooms/$roomID/invite?access_token=$usertoken2"
echo "Invited $USER1_NAME"

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
if ! command -v apk &>/dev/null; then
apt update && apt install -y -qq docker.io ldnsutils grep scrcpy ffmpeg
else
apk update && apk add docker drill grep scrcpy ffmpeg
fi

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
chmod 777 -R /dev/kvm chmod 777 -R /dev/kvm
adb start-server adb start-server
emulator -avd test -no-audio -no-boot-anim -no-window -accel on -gpu swiftshader_indirect emulator -avd test -wipe-data -no-audio -no-boot-anim -no-window -accel on -gpu swiftshader_indirect