Compare commits

...

9 Commits

8 changed files with 282 additions and 107 deletions

View File

@ -23,6 +23,8 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
curly_braces_in_flow_control_structures: false
avoid_print: false
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -1,4 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="zenith" android:label="zenith"
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:zenith/zenith_client_provider.dart';
class ChatsPage extends StatefulWidget {
const ChatsPage({super.key});
@override
State<ChatsPage> createState() => _ChatsPageState();
}
class _ChatsPageState extends State<ChatsPage> {
final serverController = TextEditingController();
final usernameController = TextEditingController();
final passwordController = TextEditingController();
late Client client;
bool roomsLoading = false;
@override
void initState() {
super.initState();
client = Provider.of<ZenithClientProvider>(context, listen: false).client;
client.onRoomState.stream.listen((event) {
print(event.type);
});
}
void sendMessage() {}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Zenith"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: roomsLoading
? const CircularProgressIndicator()
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text("${client.rooms.length} Rooms.")],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: sendMessage,
tooltip: 'Send message',
child: const Icon(Icons.send),
),
);
}
}

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:zenith/chats.dart'; import 'package:zenith/rooms.dart';
import 'package:zenith/zenith_client_provider.dart'; import 'package:zenith/zenith_client_provider.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
@ -16,11 +15,36 @@ class _LoginPageState extends State<LoginPage> {
final serverController = TextEditingController(); final serverController = TextEditingController();
final usernameController = TextEditingController(); final usernameController = TextEditingController();
final passwordController = TextEditingController(); final passwordController = TextEditingController();
bool loggingIn = false;
String failedMessage = '';
void connectMatrix() async { void connectMatrix() async {
final provider = Provider.of<ZenithClientProvider>(context, listen: false); setState(() {
await provider.initialize(serverController.text, usernameController.text, loggingIn = true;
passwordController.text); });
try {
final provider =
Provider.of<ZenithClientProvider>(context, listen: false);
await provider.initialize(serverController.text, usernameController.text,
passwordController.text);
} catch (error) {
print("Error signing in $error");
setState(() {
failedMessage = error.toString();
});
Future.delayed(const Duration(seconds: 10), () {
setState(() {
failedMessage = '';
});
});
return;
} finally {
setState(() {
loggingIn = false;
});
}
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
prefs.setString("homeserver", serverController.text); prefs.setString("homeserver", serverController.text);
@ -30,7 +54,7 @@ class _LoginPageState extends State<LoginPage> {
if (!mounted) return; if (!mounted) return;
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute(builder: (context) => const ChatsPage()), MaterialPageRoute(builder: (context) => const RoomsPage()),
(route) => false); (route) => false);
} }
@ -41,40 +65,47 @@ class _LoginPageState extends State<LoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Consumer<ZenithClientProvider>(
appBar: AppBar( builder: (context, provider, child) => Scaffold(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, appBar: AppBar(
title: const Text("Login"), backgroundColor: Theme.of(context).colorScheme.inversePrimary,
), title: const Text("Login"),
body: Padding( ),
padding: const EdgeInsets.all(16.0), body: Padding(
child: Center( padding: const EdgeInsets.all(16.0),
child: Column( child: Center(
mainAxisAlignment: MainAxisAlignment.center, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
TextFormField( children: [
controller: serverController, TextFormField(
decoration: const InputDecoration( controller: serverController,
labelText: 'Server', hintText: 'https://matrix.org'), decoration: const InputDecoration(
), labelText: 'Server', hintText: 'https://matrix.org'),
TextFormField( ),
controller: usernameController, TextFormField(
decoration: const InputDecoration( controller: usernameController,
labelText: 'Username', hintText: 'john'), decoration: const InputDecoration(
), labelText: 'Username', hintText: 'john'),
TextFormField( ),
controller: passwordController, TextFormField(
decoration: const InputDecoration(labelText: 'Password'), controller: passwordController,
obscureText: true, decoration: const InputDecoration(labelText: 'Password'),
), obscureText: true,
], onFieldSubmitted: (value) => connectMatrix(),
),
Text(failedMessage,
style: Theme.of(context).textTheme.headlineSmall)
],
),
), ),
), ),
), floatingActionButton: loggingIn
floatingActionButton: FloatingActionButton( ? const CircularProgressIndicator()
onPressed: connectMatrix, : FloatingActionButton(
tooltip: 'Log in', onPressed: connectMatrix,
child: const Icon(Icons.login), tooltip: 'Log in',
child: const Icon(Icons.login),
),
), ),
); );
} }

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:zenith/chats.dart'; import 'package:zenith/rooms.dart';
import 'package:zenith/login.dart'; import 'package:zenith/login.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:zenith/zenith_client_provider.dart'; import 'package:zenith/zenith_client_provider.dart';
@ -66,7 +66,7 @@ class _MyHomePageState extends State<MyHomePage> {
if (!savedCreds) { if (!savedCreds) {
return const LoginPage(); return const LoginPage();
} else { } else {
return const ChatsPage(); return const RoomsPage();
} }
} }
} }

90
lib/room.dart Normal file
View File

@ -0,0 +1,90 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class RoomPage extends StatefulWidget {
const RoomPage({super.key, required this.room});
final Room room;
@override
State<RoomPage> createState() => _RoomPageState();
}
class _RoomPageState extends State<RoomPage> {
Timeline? timeline;
final chatController = TextEditingController();
StreamSubscription? updateListener;
void updateTimeline() async {
final newTimeline =
await widget.room.getTimeline(eventContextId: widget.room.fullyRead);
setState(() {
timeline = newTimeline;
});
}
@override
void initState() {
super.initState();
updateTimeline();
updateListener = widget.room.onUpdate.stream.listen((event) {
updateTimeline();
});
}
@override
void dispose() {
super.dispose();
updateListener?.cancel();
}
void sendMessage() {}
List<Widget> getChildren() {
if (timeline == null) return [const CircularProgressIndicator()];
return [
Expanded(
child: ListView.builder(
itemCount: timeline?.events.length,
reverse: true,
itemBuilder: (context, index) => ListTile(
title: Text(timeline!.events[index].senderFromMemoryOrFallback
.displayName ??
""),
subtitle: Text(timeline!.events[index].body),
)),
),
TextFormField(
controller: chatController,
decoration: const InputDecoration(hintText: 'Message'),
onFieldSubmitted: (value) async {
print("Sending text event $value to room ${widget.room.id}...");
await widget.room.sendTextEvent(value);
chatController.text = '';
},
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.room.getLocalizedDisplayname()),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: getChildren(),
)),
floatingActionButton: FloatingActionButton(
onPressed: sendMessage,
tooltip: 'Send message',
child: const Icon(Icons.send),
),
);
}
}

102
lib/rooms.dart Normal file
View File

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:zenith/login.dart';
import 'package:zenith/room.dart';
import 'package:zenith/zenith_client_provider.dart';
class RoomsPage extends StatefulWidget {
const RoomsPage({super.key});
@override
State<RoomsPage> createState() => _RoomsPageState();
}
class _RoomsPageState extends State<RoomsPage> {
@override
void initState() {
super.initState();
}
void sendMessage() {}
void viewRoom(Room room) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => RoomPage(room: room)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Zenith"),
actions: [
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Log out?"),
actions: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Cancel")),
ElevatedButton(
child: const Text("Confirm"),
onPressed: () async {
print("Logging out...");
final prefs = await SharedPreferences.getInstance();
prefs.remove("homeserver");
prefs.remove("username");
prefs.remove("password");
if (!mounted) return;
final provider = Provider.of<ZenithClientProvider>(
context,
listen: false);
await provider.client.logout();
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => const LoginPage()),
(route) => false);
},
)
],
),
);
},
icon: const Icon(Icons.logout))
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(child: Consumer<ZenithClientProvider>(
builder: (context, provider, child) {
if (provider.loading)
return const CircularProgressIndicator();
else
return ListView.builder(
itemCount: provider.client.rooms.length,
itemBuilder: (context, index) => ListTile(
title: Text(provider.client.rooms[index]
.getLocalizedDisplayname()),
onTap: () => viewRoom(provider.client.rooms[index]),
));
},
)),
),
floatingActionButton: FloatingActionButton(
onPressed: sendMessage,
tooltip: 'Send message',
child: const Icon(Icons.send),
),
);
}
}

View File

@ -3,23 +3,28 @@ import 'package:matrix/matrix.dart';
class ZenithClientProvider extends ChangeNotifier { class ZenithClientProvider extends ChangeNotifier {
late Client _client; late Client _client;
bool _loading = true;
Client get client => _client; Client get client => _client;
void setClient(Client newClient) { bool get loading => _loading;
_client = newClient;
notifyListeners();
}
Future<void> initialize( Future<void> initialize(
String homeserver, String username, String password) async { String homeserver, String username, String password) async {
_loading = true;
_client = Client("zenith"); _client = Client("zenith");
print("Checking homeserver..."); try {
await client.checkHomeserver(Uri.parse(homeserver)); print("Checking homeserver...");
print("Logging in..."); await client.checkHomeserver(Uri.parse(homeserver));
await client.login(LoginType.mLoginPassword, print("Logging in...");
identifier: AuthenticationUserIdentifier(user: username), await client.login(LoginType.mLoginPassword,
password: password); identifier: AuthenticationUserIdentifier(user: username),
password: password);
await client.roomsLoading;
await client.accountDataLoading;
} finally {
_loading = false;
}
notifyListeners(); notifyListeners();
} }
} }