chore: add integration tests

Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
TheOneWithTheBraid 2022-12-29 15:02:29 +01:00
parent 6b3252b6ad
commit ed075a35b6
18 changed files with 491 additions and 103 deletions

View File

@ -1,7 +1,9 @@
variables:
FLUTTER_VERSION: 3.3.9
image: cirrusci/flutter:${FLUTTER_VERSION}
image:
name: cirrusci/flutter:${FLUTTER_VERSION}
pull_policy: if-not-present
.shared_windows_runners:
tags:
@ -16,7 +18,7 @@ stages:
code_analyze:
stage: test
script: [./scripts/code_analyze.sh]
script: [ ./scripts/code_analyze.sh ]
artifacts:
reports:
codequality: code-quality-report.json
@ -26,13 +28,13 @@ code_analyze:
widget_test:
stage: test
script: [flutter test]
script: [ flutter test ]
tags:
- docker
- famedly
# 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}
stage: test
services:
@ -49,15 +51,13 @@ widget_test:
FF_NETWORK_PER_BUILD: "true"
# Tell docker CLI how to talk to Docker daemon.
DOCKER_HOST: tcp://docker:2375/
# Use the overlayfs driver for improved performance.
DOCKER_DRIVER: overlay2
# Use the btrfs driver for improved performance.
DOCKER_DRIVER: btrfs
# Disable TLS since we're running inside local network.
DOCKER_TLS_CERTDIR: ""
HOMESERVER: "docker"
HOMESERVER: docker
before_script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- scripts/integration-prepare-alpine.sh
- scripts/integration-prepare-host.sh
# create test user environment variables
- source scripts/integration-create-environment-variables.sh
# create Synapse instance
@ -65,31 +65,49 @@ widget_test:
# properly set the homeserver IP and create test users
- scripts/integration-prepare-homeserver.sh
script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- flutter pub get
- flutter test integration_test
timeout: 20m
- 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 )
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:
- docker
- famedly
# integration tests for Linux builds
### disabled because of Linux headless issues
.integration_test_linux:
extends: .integration_test
image: cirrusci/flutter:${FLUTTER_VERSION}
extends: integration_test
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 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
.integration_test_proprietary:
extends: .integration_test
integration_test_proprietary:
extends: integration_test
script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- git apply ./scripts/enable-android-google-services.patch
- 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:
matrix:
- FLAVOR:
@ -99,9 +117,9 @@ widget_test:
stage: test
before_script:
- |
if [ "$FLAVOR" == "proprietary" ]; then
git apply ./scripts/enable-android-google-services.patch
fi
if [ "$FLAVOR" == "proprietary" ]; then
git apply ./scripts/enable-android-google-services.patch
fi
script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
@ -115,8 +133,8 @@ widget_test:
build_web:
stage: build
before_script:
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh]
script: [./scripts/build-web.sh]
[ sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh ]
script: [ ./scripts/build-web.sh ]
artifacts:
paths:
- build/web/
@ -166,7 +184,7 @@ build_windows:
build_android_debug:
stage: build
script: [./scripts/build-android-debug.sh]
script: [ ./scripts/build-android-debug.sh ]
artifacts:
when: on_success
paths:
@ -183,7 +201,7 @@ build_android_apk:
before_script:
- git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh
script: [./scripts/build-android-apk.sh]
script: [ ./scripts/build-android-apk.sh ]
artifacts:
when: on_success
paths:
@ -200,7 +218,7 @@ deploy_playstore_internal:
before_script:
- git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh
script: [./scripts/release-playstore-beta.sh]
script: [ ./scripts/release-playstore-beta.sh ]
artifacts:
when: on_success
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,
]
script: [./scripts/build-linux.sh]
script: [ ./scripts/build-linux.sh ]
tags:
- docker
- famedly
@ -278,9 +296,9 @@ build_linux_x86:
build_linux_arm64:
stage: build
before_script: [flutter upgrade]
script: [./scripts/build-linux.sh]
tags: [docker_arm64]
before_script: [ flutter upgrade ]
script: [ ./scripts/build-linux.sh ]
tags: [ docker_arm64 ]
only:
- main
- tags
@ -292,7 +310,7 @@ build_linux_arm64:
update_dependencies:
stage: build
needs: []
needs: [ ]
tags:
- docker
only:
@ -374,7 +392,7 @@ deploy_playstore:
before_script:
- git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh
script: [./scripts/release-playstore.sh]
script: [ ./scripts/release-playstore.sh ]
resource_group: playstore_release
only:
- 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_test/flutter_test.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:integration_test/integration_test.dart';
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';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Integration Test', () {
testWidgets('Test if the app starts', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
group(
'Integration Test',
() {
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);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
try {
await tester.waitFor(
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
try {
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
} catch (e) {
log('Registration is not allowed. Proceeding with login...');
}
await tester.pumpAndSettle();
final inputs = find.byType(TextField);
await tester.enterText(inputs.first, Users.user1.name);
await tester.enterText(inputs.last, Users.user1.password);
await tester.testTextInput.receiveAction(TextInputAction.done);
});
});
await tester.waitFor(find.byType(ChatView));
await tester.enterText(find.byType(TextField).last, 'Test');
await tester.pumpAndSettle();
try {
await tester.waitFor(find.byIcon(Icons.send_outlined));
await tester.tap(find.byIcon(Icons.send_outlined));
} catch (_) {
await tester.testTextInput.receiveAction(TextInputAction.done);
}
await tester.pumpAndSettle();
await tester.waitFor(find.text('Test'));
await tester.pumpAndSettle();
},
);
},
);
}

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 {
const Users._();
static final user1 = User(
Platform.environment['USER1_NAME'] ?? 'alice',
Platform.environment['USER1_PW'] ?? 'AliceInWonderland',
static const user1 = User(
String.fromEnvironment(
'USER1_NAME',
defaultValue: 'alice',
),
String.fromEnvironment(
'USER1_PW',
defaultValue: 'AliceInWonderland',
),
);
static final user2 = User(
Platform.environment['USER2_NAME'] ?? 'bob',
Platform.environment['USER2_PW'] ?? 'JoWirSchaffenDas',
static const user2 = User(
String.fromEnvironment(
'USER2_NAME',
defaultValue: 'bob',
),
String.fromEnvironment(
'USER2_PW',
defaultValue: 'JoWirSchaffenDas',
),
);
}
@ -20,5 +30,7 @@ class User {
const User(this.name, this.password);
}
final homeserver =
'http://${Platform.environment['HOMESERVER'] ?? 'localhost'}';
const homeserver = 'http://${const String.fromEnvironment(
'HOMESERVER',
defaultValue: 'localhost',
)}';

View File

@ -104,8 +104,13 @@ class ClientChooserButton extends StatelessWidget {
.map(
(client) => PopupMenuItem(
value: client,
child: FutureBuilder<Profile>(
future: client!.fetchOwnProfile(),
child: FutureBuilder<Profile?>(
// 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(
children: [
Avatar(

View File

@ -28,6 +28,7 @@ class ConnectPageView extends StatelessWidget {
),
),
body: ListView(
key: const Key('ConnectPageListView'),
children: [
if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
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:matrix/matrix.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/platform_infos.dart';
import 'login_view.dart';
@ -30,7 +31,7 @@ class LoginController extends State<Login> {
void toggleShowPassword() =>
setState(() => showPassword = !loading && !showPassword);
void login([_]) async {
void login() async {
final matrix = Matrix.of(context);
if (usernameController.text.isEmpty) {
setState(() => usernameError = L10n.of(context)!.pleaseEnterYourUsername);
@ -48,6 +49,9 @@ class LoginController extends State<Login> {
}
setState(() => loading = true);
_coolDown?.cancel();
try {
final username = usernameController.text;
AuthenticationIdentifier identifier;
@ -97,8 +101,8 @@ class LoginController extends State<Login> {
void _checkWellKnown(String userId) async {
if (mounted) setState(() => usernameError = null);
if (!userId.isValidMatrixId) return;
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
try {
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
var newDomain = Uri.https(userId.domain!, '');
Matrix.of(context).getLoginClient().homeserver = newDomain;
DiscoveryInformation? wellKnownInformation;
@ -112,14 +116,11 @@ class LoginController extends State<Login> {
// do nothing, newDomain is already set to a reasonable fallback
}
if (newDomain != oldHomeserver) {
await showFutureLoadingDialog(
context: context,
// do nothing if we error, we'll handle it below
future: () => Matrix.of(context)
.getLoginClient()
.checkHomeserver(newDomain)
.catchError((e) {}),
);
Matrix.of(context)
.getLoginClient()
.checkHomeserver(newDomain)
.catchError((e) {});
if (Matrix.of(context).getLoginClient().homeserver == null) {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
// okay, the server we checked does not appear to be a matrix server
@ -140,15 +141,18 @@ class LoginController extends State<Login> {
return;
}
}
if (mounted) setState(() => usernameError = null);
usernameError = null;
if (mounted) setState(() {});
} else {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
if (mounted) {
setState(() =>
Matrix.of(context).getLoginClient().homeserver = oldHomeserver);
setState(() {});
}
}
} 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,
textInputAction: TextInputAction.go,
obscureText: !controller.showPassword,
onSubmitted: controller.login,
onSubmitted: (_) => controller.login(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outlined),
errorText: controller.passwordError,
@ -87,9 +87,7 @@ class LoginView extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.loading
? null
: () => controller.login(context),
onPressed: controller.loading ? null : controller.login,
icon: const Icon(Icons.login_outlined),
label: controller.loading
? const LinearProgressIndicator()

View File

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

View File

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

View File

@ -327,15 +327,15 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
);
if (state != LoginState.loggedIn) {
widget.router!.currentState!.to(
widget.router?.currentState?.to(
'/rooms',
queryParameters: widget.router!.currentState!.queryParameters,
queryParameters: widget.router?.currentState?.queryParameters ?? {},
);
}
} else {
widget.router!.currentState!.to(
widget.router?.currentState?.to(
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 {
final String userId;
final BuildContext outerContext;
const ProfileBottomSheet({
required this.userId,
required this.outerContext,
@ -78,6 +79,13 @@ class ProfileBottomSheet extends StatelessWidget {
mxContent: profile.avatarUrl,
defaultIcon: Icons.account_circle_outlined,
client: Matrix.of(context).client,
placeholder: (context) => Center(
child: Text(
userId.localpart ?? userId,
style:
Theme.of(context).textTheme.headline3,
),
),
),
),
ListTile(

View File

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

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\":\"$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
chmod 777 -R /dev/kvm
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