Compare commits

...
28 Commits
Author SHA1 Message Date
brandon.presley 11de9ccfe8 Turn off autocorrect/capitalization for login 2023-12-30 16:17:05 +13:00
brandon.presley 128a1bf35f Add persistence to matrix Client 2023-12-30 16:11:20 +13:00
brandon.presley a552c30ba0 Remove logging of rooms with "Group with" 2023-12-30 16:00:54 +13:00
brandon.presley 2039b4e90d Remove custom code for renaming groups
Was something for my local matrix
environment, committed
accidentally.
2023-12-30 15:54:30 +13:00
brandon.presley 1aa73bbd7e Add failed attempt to fix all room names 2023-12-30 15:16:42 +13:00
brandon.presley 6ca8f646b2 Fix null being on chat messages author temporarily 2023-12-30 14:44:35 +13:00
brandon.presley a40dd74b96 Close chat faster on send 2023-12-30 12:49:14 +13:00
brandon.presley 10b84d1521 Hide "Group with" from room names 2023-12-30 12:47:48 +13:00
brandon.presley 8ca788ccdd Add button to change image of chat 2023-12-30 12:41:40 +13:00
brandon.presley 7c1e18c4e8 Add avatar to rooms 2023-12-30 11:47:27 +13:00
brandon.presley 76411aa827 Fix login flow and room name updates
1. Logging in with saved credentials should
just go straight to rooms page without
flickering the login page
2. Rooms should be updated if an event
fires for their names changing
2023-12-30 11:39:34 +13:00
brandon.presley 27b7f79d74 Only login if the client is logged out 2023-12-30 11:23:57 +13:00
brandon.presley f229c0371b Save username/password instead of tokens
The tokens never worked. I don't know what i'm doing wrong but fluffychat basically just logs in and never tries to save anything. Im not sure how im meant to do this.
2023-12-30 11:17:21 +13:00
brandon.presley 1cd8ba41ae Reduce nesting of login.dart 2023-12-30 11:16:38 +13:00
brandon.presley 3728172844 Tried to set up auth tokens
I always get invalid token error
when I try to login with mLoginToken type
2023-12-29 19:31:35 +13:00
brandon.presley ea7587fe2d Only show messages in room view 2023-12-29 17:19:40 +13:00
brandon.presley 1376580e7e Add profile images to chats 2023-12-29 17:10:47 +13:00
brandon.presley 5fbc8bee42 Add launch.json but debugging still doesnt work 2023-12-29 15:12:36 +13:00
brandon.presley 9c93ea1008 Move loading into fab button of login 2023-12-29 15:12:10 +13:00
brandon.presley e478552587 Add log out button 2023-12-29 14:50:09 +13:00
brandon.presley 5c4526f1fc Add loader for logging in 2023-12-29 14:50:00 +13:00
brandon.presley 302f46804e Include internet permissions in manifest 2023-12-29 11:31:33 +13:00
brandon.presley 071df18b81 Wait for rooms and account data to load 2023-12-29 11:31:26 +13:00
brandon.presley c5ae0f0e85 Add ability to send messages 2023-12-29 11:31:18 +13:00
brandon.presley 705aeb568f Remove unused code 2023-12-29 10:53:43 +13:00
brandon.presley 3352939009 View messages for a room 2023-12-29 10:40:09 +13:00
brandon.presley a6306ceddc List rooms in chats.dart 2023-12-29 09:20:50 +13:00
brandon.presley 112fe960be Update analysis options 2023-12-29 09:20:28 +13:00
12 changed files with 428 additions and 127 deletions
+25
View File
@@ -0,0 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "zenith",
"request": "launch",
"type": "dart",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/lib/main.dart",
"flutterMode": "debug"
},
{
"name": "zenith (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "zenith (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}
+2
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
+2
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}"
-57
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),
),
);
}
}
+51 -10
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.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 +16,42 @@ 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 login() async {
setState(() {
loggingIn = true;
});
void connectMatrix() async {
final provider = Provider.of<ZenithClientProvider>(context, listen: false); final provider = Provider.of<ZenithClientProvider>(context, listen: false);
await provider.initialize(serverController.text, usernameController.text,
passwordController.text); try {
await provider.client.checkHomeserver(Uri.parse(serverController.text));
await provider.client.login(
LoginType.mLoginPassword,
password: passwordController.text,
identifier: AuthenticationUserIdentifier(user: usernameController.text),
refreshToken: true,
);
} catch (error) {
print(error);
setState(() {
failedMessage = error.toString();
loggingIn = false;
});
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 +61,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,41 +72,51 @@ class _LoginPageState extends State<LoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Consumer<ZenithClientProvider>(
builder: (context, provider, child) => Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Login"), title: const Text("Login"),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0),
child: Center( child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
TextFormField( TextFormField(
controller: serverController, controller: serverController,
autocorrect: false,
textCapitalization: TextCapitalization.none,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Server', hintText: 'https://matrix.org'), labelText: 'Server', hintText: 'https://matrix.org'),
), ),
TextFormField( TextFormField(
controller: usernameController, controller: usernameController,
autocorrect: false,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Username', hintText: 'john'), labelText: 'Username', hintText: 'john'),
), ),
TextFormField( TextFormField(
controller: passwordController, controller: passwordController,
autocorrect: false,
decoration: const InputDecoration(labelText: 'Password'), decoration: const InputDecoration(labelText: 'Password'),
obscureText: true, obscureText: true,
onFieldSubmitted: (value) => login(),
), ),
Text(failedMessage,
style: Theme.of(context).textTheme.headlineSmall)
], ],
), ),
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: connectMatrix, onPressed: loggingIn ? null : login,
tooltip: 'Log in', tooltip: 'Log in',
child: const Icon(Icons.login), child: loggingIn
), ? const CircularProgressIndicator()
: const Icon(Icons.login),
)),
); );
} }
} }
+35 -10
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';
@@ -37,7 +37,8 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
bool savedCreds = false; bool showRooms = false;
String errorMessage = "";
void getCreds() async { void getCreds() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@@ -46,13 +47,25 @@ class _MyHomePageState extends State<MyHomePage> {
final password = prefs.getString("password"); final password = prefs.getString("password");
if (homeserver == null || username == null || password == null) return; if (homeserver == null || username == null || password == null) return;
setState(() {
savedCreds = true;
});
if (!mounted) return; if (!mounted) return;
final provider = Provider.of<ZenithClientProvider>(context, listen: false); final provider = Provider.of<ZenithClientProvider>(context, listen: false);
await provider.initialize(homeserver, username, password); setState(() {
showRooms = true;
});
try {
await provider.client.checkHomeserver(Uri.parse(homeserver));
if (!provider.client.isLogged())
await provider.client.login(LoginType.mLoginPassword,
password: password,
identifier: AuthenticationUserIdentifier(user: username));
} catch (error) {
print(error);
setState(() {
errorMessage = error.toString();
});
}
} }
@override @override
@@ -63,10 +76,22 @@ class _MyHomePageState extends State<MyHomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!savedCreds) { if (errorMessage.isNotEmpty)
return Column(
children: [
ElevatedButton(
onPressed: () {
setState(() {
errorMessage = "";
});
},
child: const Text("Clear")),
ErrorWidget(errorMessage),
],
);
if (showRooms)
return const RoomsPage();
else
return const LoginPage(); return const LoginPage();
} else {
return const ChatsPage();
}
} }
} }
+118
View File
@@ -0,0 +1,118 @@
import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
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();
});
}
void setAvatar() async {
final participants = widget.room.getParticipants();
final picked = await FilePicker.platform.pickFiles(type: FileType.image);
if (picked == null) return;
final file = File(picked.files.single.path!);
final bytes = await file.readAsBytes();
final uri = await widget.room.client.uploadContent(bytes);
await widget.room.client.setAvatarUrl(participants[0].id, uri);
}
@override
void dispose() {
super.dispose();
updateListener?.cancel();
}
void sendMessage() {}
List<Event>? get messages => timeline?.events
.where((element) => element.type == EventTypes.Message)
.toList();
List<Widget> getChildren() {
if (timeline == null) return [const CircularProgressIndicator()];
return [
Expanded(
child: ListView.builder(
itemCount: messages?.length,
reverse: true,
itemBuilder: (context, index) => ListTile(
title: Text(
messages![index].senderFromMemoryOrFallback.displayName ??
""),
subtitle: Text(messages![index].body),
leading:
messages![index].senderFromMemoryOrFallback.avatarUrl !=
null
? CircleAvatar(
foregroundImage: NetworkImage(messages![index]
.senderFromMemoryOrFallback
.avatarUrl!
.getThumbnail(widget.room.client,
width: 50, height: 50)
.toString()))
: null,
)),
),
TextFormField(
controller: chatController,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.send,
decoration: const InputDecoration(hintText: 'Message'),
onFieldSubmitted: (value) async {
chatController.text = '';
await widget.room.sendTextEvent(value);
},
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.room
.getLocalizedDisplayname()
.replaceFirst(RegExp("Group with "), "")),
actions: [
IconButton(onPressed: setAvatar, icon: const Icon(Icons.image))
],
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: getChildren(),
)),
);
}
}
+113
View File
@@ -0,0 +1,113 @@
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> {
bool loadingRooms = true;
@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(8.0),
child: Center(child: Consumer<ZenithClientProvider>(
builder: (context, provider, child) {
provider.client.onRoomState.stream.listen((event) {
setState(() {
loadingRooms = false;
});
});
if (provider.client.rooms.isEmpty)
return const CircularProgressIndicator();
else
return ListView.builder(
itemCount: provider.client.rooms.length,
itemBuilder: (context, index) => ListTile(
title: Text(provider.client.rooms[index]
.getLocalizedDisplayname()
.replaceFirst(RegExp("Group with "), "")),
leading: CircleAvatar(
foregroundImage: NetworkImage(provider
.client.rooms[index].avatar
?.getThumbnail(provider.client,
width: 50, height: 50)
.toString() ??
"")),
onTap: () => viewRoom(provider.client.rooms[index]),
));
},
)),
),
);
}
}
+8 -20
View File
@@ -1,25 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
class ZenithClientProvider extends ChangeNotifier { class ZenithClientProvider extends ChangeNotifier {
late Client _client; final client = Client("zenith", databaseBuilder: (_) async {
final dir =
Client get client => _client; await getApplicationSupportDirectory(); // Recommend path_provider package
final db = HiveCollectionsDatabase('matrix_example_chat', dir.path);
void setClient(Client newClient) { await db.open();
_client = newClient; return db;
notifyListeners(); });
}
Future<void> initialize(
String homeserver, String username, String password) async {
_client = Client("zenith");
print("Checking homeserver...");
await client.checkHomeserver(Uri.parse(homeserver));
print("Logging in...");
await client.login(LoginType.mLoginPassword,
identifier: AuthenticationUserIdentifier(user: username),
password: password);
notifyListeners();
}
} }
@@ -5,8 +5,10 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }
+41 -1
View File
@@ -145,6 +145,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.0"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6"
url: "https://pub.dev"
source: hosted
version: "6.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -174,6 +182,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.0" version: "0.3.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
url: "https://pub.dev"
source: hosted
version: "2.0.17"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -328,6 +344,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.3" version: "1.8.3"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -607,4 +647,4 @@ packages:
version: "6.3.0" version: "6.3.0"
sdks: sdks:
dart: ">=3.1.5 <4.0.0" dart: ">=3.1.5 <4.0.0"
flutter: ">=3.7.0" flutter: ">=3.10.0"
+2
View File
@@ -40,6 +40,8 @@ dependencies:
flutter_openssl_crypto: ^0.3.0 flutter_openssl_crypto: ^0.3.0
provider: ^6.1.1 provider: ^6.1.1
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
file_picker: ^6.1.1
path_provider: ^2.1.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: