start using drift

This commit is contained in:
otsmr 2025-03-09 00:06:57 +01:00
parent 00ad654660
commit f7306fe7db
48 changed files with 3138 additions and 807 deletions

View file

@ -1,5 +1,5 @@
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart'; import 'package:twonly/src/providers/db_provider.dart';
late DbProvider dbProvider;
late ApiProvider apiProvider; late ApiProvider apiProvider;
late DbProvider dbProvider;

View file

@ -2,14 +2,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/src/providers/download_change_provider.dart'; import 'package:twonly/src/providers/db_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/providers/settings_change_provider.dart';
import 'package:twonly/src/services/fcm_service.dart'; import 'package:twonly/src/services/fcm_service.dart';
@ -49,9 +47,10 @@ void main() async {
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (_) => MessagesChangeProvider()), Provider<TwonlyDatabase>(
ChangeNotifierProvider(create: (_) => DownloadChangeProvider()), create: (context) => TwonlyDatabase(),
ChangeNotifierProvider(create: (_) => ContactChangeProvider()), dispose: (context, db) => db.close(),
),
ChangeNotifierProvider(create: (_) => SendNextMediaTo()), ChangeNotifierProvider(create: (_) => SendNextMediaTo()),
ChangeNotifierProvider(create: (_) => settingsController), ChangeNotifierProvider(create: (_) => settingsController),
], ],

View file

@ -1,9 +1,6 @@
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/components/connection_state.dart'; import 'package:twonly/src/components/connection_state.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/providers/settings_change_provider.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/onboarding/onboarding_view.dart'; import 'package:twonly/src/views/onboarding/onboarding_view.dart';
@ -23,9 +20,6 @@ Function(bool) globalCallbackConnectionState = (a) {};
bool globalIsAppInBackground = true; bool globalIsAppInBackground = true;
// these two callbacks are called on updated to the corresponding database // these two callbacks are called on updated to the corresponding database
Function globalCallBackOnContactChange = () {};
Future Function(int, int?) globalCallBackOnMessageChange = (a, b) async {};
Function(List<int>, bool) globalCallBackOnDownloadChange = (a, b) {};
/// The Widget that configures your application. /// The Widget that configures your application.
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
@ -45,10 +39,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
globalIsAppInBackground = false; globalIsAppInBackground = false;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
// init change provider to load data from the database
context.read<ContactChangeProvider>().update();
context.read<MessagesChangeProvider>().init();
// register global callbacks to the widget tree // register global callbacks to the widget tree
globalCallbackConnectionState = (isConnected) { globalCallbackConnectionState = (isConnected) {
setState(() { setState(() {
@ -56,20 +46,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}); });
}; };
globalCallBackOnContactChange = () {
context.read<ContactChangeProvider>().update();
};
globalCallBackOnDownloadChange = (token, add) {
context.read<DownloadChangeProvider>().update(token, add);
};
globalCallBackOnMessageChange = (userId, messageId) async {
await context
.read<MessagesChangeProvider>()
.updateLastMessageFor(userId, messageId);
};
// WidgetsBinding.instance.addPostFrameCallback((_) { // WidgetsBinding.instance.addPostFrameCallback((_) {
// _requestPermissions(); // _requestPermissions();
// _initService(); // _initService();
@ -124,8 +100,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
if (wasPaused) { if (wasPaused) {
globalIsAppInBackground = false; globalIsAppInBackground = false;
apiProvider.connect(); apiProvider.connect();
context.read<ContactChangeProvider>().update();
context.read<MessagesChangeProvider>().init();
// _stopService(); // _stopService();
} }
} else if (state == AppLifecycleState.paused) { } else if (state == AppLifecycleState.paused) {
@ -145,9 +119,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
// disable globalCallbacks to the flutter tree // disable globalCallbacks to the flutter tree
globalCallbackConnectionState = (a) {}; globalCallbackConnectionState = (a) {};
globalCallBackOnDownloadChange = (a, b) {};
globalCallBackOnContactChange = () {};
globalCallBackOnMessageChange = (a, b) async {};
super.dispose(); super.dispose();
} }

View file

@ -1,19 +1,17 @@
import 'dart:collection'; import 'dart:collection';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/components/verified_shield.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/components/flame.dart'; import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/contacts_model.dart';
class BestFriendsSelector extends StatelessWidget { class BestFriendsSelector extends StatelessWidget {
final List<Contact> users; final List<Contact> users;
final Function(Int64, bool) updateStatus; final Function(int, bool) updateStatus;
final HashSet<Int64> selectedUserIds; final HashSet<int> selectedUserIds;
final int maxTotalMediaCounter; final int maxTotalMediaCounter;
final bool isRealTwonly; final bool isRealTwonly;
@ -80,7 +78,7 @@ class BestFriendsSelector extends StatelessWidget {
class UserCheckbox extends StatelessWidget { class UserCheckbox extends StatelessWidget {
final Contact user; final Contact user;
final Function(Int64, bool) onChanged; final Function(int, bool) onChanged;
final bool isChecked; final bool isChecked;
final bool isRealTwonly; final bool isRealTwonly;
final int maxTotalMediaCounter; final int maxTotalMediaCounter;
@ -96,10 +94,7 @@ class UserCheckbox extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int flameCounter = context String displayName = getContactDisplayName(user);
.watch<MessagesChangeProvider>()
.flamesCounter[user.userId.toInt()] ??
0;
return Container( return Container(
padding: padding:
@ -120,8 +115,8 @@ class UserCheckbox extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
InitialsAvatar( InitialsAvatar(
displayName,
fontSize: 12, fontSize: 12,
displayName: user.displayName,
), ),
SizedBox(width: 8), SizedBox(width: 8),
Column( Column(
@ -138,16 +133,23 @@ class UserCheckbox extends StatelessWidget {
size: 12, size: 12,
)), )),
Text( Text(
user.displayName.length > 10 displayName.length > 10
? '${user.displayName.substring(0, 10)}...' ? '${displayName.substring(0, 10)}...'
: user.displayName, : displayName,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
if (flameCounter > 0) StreamBuilder(
FlameCounterWidget( stream: context.db.watchFlameCounter(user.userId),
user, flameCounter, maxTotalMediaCounter), builder: (context, snapshot) {
if (!snapshot.hasData && snapshot.data! != 0) {
return Container();
}
return FlameCounterWidget(
user, snapshot.data!, maxTotalMediaCounter);
},
)
], ],
), ),
Expanded(child: Container()), Expanded(child: Container()),

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/components/animate_icon.dart'; import 'package:twonly/src/components/animate_icon.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/database/database.dart';
class FlameCounterWidget extends StatelessWidget { class FlameCounterWidget extends StatelessWidget {
final Contact user; final Contact user;

View file

@ -4,8 +4,7 @@ class InitialsAvatar extends StatelessWidget {
final String displayName; final String displayName;
final double? fontSize; final double? fontSize;
const InitialsAvatar( const InitialsAvatar(this.displayName, {super.key, this.fontSize = 20});
{super.key, required this.displayName, this.fontSize = 20});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -1,9 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
enum MessageSendState { enum MessageSendState {
@ -15,21 +15,75 @@ enum MessageSendState {
sending, sending,
} }
class MessageSendStateIcon extends StatelessWidget { MessageSendState messageSendStateFromMessage(Message msg) {
final DbMessage message; MessageSendState state;
if (!msg.acknowledgeByServer) {
state = MessageSendState.sending;
} else {
if (msg.messageOtherId == null) {
// message send
if (msg.openedAt == null) {
state = MessageSendState.send;
} else {
state = MessageSendState.sendOpened;
}
} else {
// message received
if (msg.openedAt == null) {
state = MessageSendState.received;
} else {
state = MessageSendState.receivedOpened;
}
}
}
return state;
}
class MessageSendStateIcon extends StatefulWidget {
final List<Message> messages;
final MainAxisAlignment mainAxisAlignment; final MainAxisAlignment mainAxisAlignment;
const MessageSendStateIcon(this.message, const MessageSendStateIcon(this.messages,
{super.key, this.mainAxisAlignment = MainAxisAlignment.end}); {super.key, this.mainAxisAlignment = MainAxisAlignment.end});
@override
State<MessageSendStateIcon> createState() => _MessageSendStateIconState();
}
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
bool containsVideo = false;
bool containsText = false;
bool containsImage = false;
@override
void initState() {
super.initState();
for (Message msg in widget.messages) {
if (msg.kind == MessageKind.textMessage) {
containsText = true;
}
if (msg.kind == MessageKind.media) {
MessageJson message =
MessageJson.fromJson(jsonDecode(msg.contentJson!));
final content = message.content;
if (content is MediaMessageContent) {
if (content.isVideo) {
containsVideo = true;
} else {
containsImage = true;
}
}
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget icon = Placeholder(); Widget icon = Placeholder();
String text = ""; String text = "";
Color color =
message.messageContent.getColor(Theme.of(context).colorScheme.primary);
Widget loaderIcon = Row( Widget loaderIcon = Row(
children: [ children: [
SizedBox( SizedBox(
@ -41,7 +95,9 @@ class MessageSendStateIcon extends StatelessWidget {
], ],
); );
switch (message.getSendState()) { MessageSendState state = messageSendStateFromMessage(message);
switch (state) {
case MessageSendState.receivedOpened: case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color); icon = Icon(Icons.crop_square, size: 14, color: color);
text = context.lang.messageSendState_Received; text = context.lang.messageSendState_Received;
@ -65,24 +121,16 @@ class MessageSendStateIcon extends StatelessWidget {
break; break;
} }
if (!message.isDownloaded) { if (message.downloadState == DownloadState.pending) {
text = context.lang.messageSendState_TapToLoad; text = context.lang.messageSendState_TapToLoad;
} }
if (message.downloadState == DownloadState.downloaded) {
bool isDownloading = false;
final content = message.messageContent;
if (message.messageReceived && content is MediaMessageContent) {
final test = context.watch<DownloadChangeProvider>().currentlyDownloading;
isDownloading = test.contains(content.downloadToken.toString());
}
if (isDownloading) {
text = context.lang.messageSendState_Loading; text = context.lang.messageSendState_Loading;
icon = loaderIcon; icon = loaderIcon;
} }
return Row( return Row(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: widget.mainAxisAlignment,
children: [ children: [
icon, icon,
const SizedBox(width: 3), const SizedBox(width: 3),

View file

@ -2,17 +2,19 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_item_details_view.dart'; import 'package:twonly/src/views/chats/chat_item_details_view.dart';
import 'package:twonly/src/views/contact/contact_verify_view.dart'; import 'package:twonly/src/views/contact/contact_verify_view.dart';
import 'package:twonly/src/views/home_view.dart'; import 'package:twonly/src/views/home_view.dart';
class UserContextMenu extends StatefulWidget { class UserContextMenu extends StatefulWidget {
final Widget child; final Widget child;
final Contact user; final Contact contact;
const UserContextMenu({super.key, required this.user, required this.child}); const UserContextMenu(
{super.key, required this.contact, required this.child});
@override @override
State<UserContextMenu> createState() => _UserContextMenuState(); State<UserContextMenu> createState() => _UserContextMenuState();
@ -22,38 +24,38 @@ class _UserContextMenuState extends State<UserContextMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieMenu( return PieMenu(
onPressed: () => print('pressed'), onPressed: () => (),
actions: [ actions: [
PieAction( PieAction(
tooltip: const Text('Verify user'), tooltip: Text(context.lang.contextMenuVerifyUser),
onSelect: () { onSelect: () {
Navigator.push(context, MaterialPageRoute( Navigator.push(context, MaterialPageRoute(
builder: (context) { builder: (context) {
return ContactVerifyView(widget.user); return ContactVerifyView(widget.contact);
}, },
)); ));
}, },
child: widget.user.verified child: widget.contact.verified
? FaIcon(FontAwesomeIcons.shieldHeart) ? FaIcon(FontAwesomeIcons.shieldHeart)
: const Icon(Icons.gpp_maybe_rounded), // Can be any widget : const Icon(Icons.gpp_maybe_rounded),
), ),
PieAction( PieAction(
tooltip: const Text('Open chat'), tooltip: Text(context.lang.contextMenuOpenChat),
onSelect: () { onSelect: () {
Navigator.push(context, MaterialPageRoute( Navigator.push(context, MaterialPageRoute(
builder: (context) { builder: (context) {
return ChatItemDetailsView(user: widget.user); return ChatItemDetailsView(user: widget.contact);
}, },
)); ));
}, },
child: const FaIcon(FontAwesomeIcons.solidComments), child: const FaIcon(FontAwesomeIcons.solidComments),
), ),
PieAction( PieAction(
tooltip: const Text('Send image'), tooltip: Text(context.lang.contextMenuSendImage),
onSelect: () { onSelect: () {
context context
.read<SendNextMediaTo>() .read<SendNextMediaTo>()
.updateSendNextMediaTo(widget.user.userId.toInt()); .updateSendNextMediaTo(widget.contact.userId.toInt());
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
}, },
child: const FaIcon(FontAwesomeIcons.camera), child: const FaIcon(FontAwesomeIcons.camera),

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/database/database.dart';
class VerifiedShield extends StatelessWidget { class VerifiedShield extends StatelessWidget {
final Contact contact; final Contact contact;

View file

@ -0,0 +1,53 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/database.dart';
class Contacts extends Table {
IntColumn get userId => integer()();
TextColumn get username => text().unique()();
TextColumn get displayName => text().nullable()();
TextColumn get nickName => text().nullable()();
BoolColumn get accepted => boolean().withDefault(Constant(false))();
BoolColumn get requested => boolean().withDefault(Constant(false))();
BoolColumn get blocked => boolean().withDefault(Constant(false))();
BoolColumn get verified => boolean().withDefault(Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get totalMediaCounter => integer().withDefault(Constant(0))();
DateTimeColumn get lastMessageSend => dateTime().nullable()();
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
DateTimeColumn get lastMessage => dateTime().nullable()();
IntColumn get flameCounter => integer().withDefault(Constant(0))();
@override
Set<Column> get primaryKey => {userId};
}
String getContactDisplayName(Contact user) {
if (user.nickName != null) {
return user.nickName!;
}
if (user.displayName != null) {
return user.displayName!;
}
return user.username;
}
int getFlameCounterFromContact(Contact contact) {
if (contact.lastMessageSend == null || contact.lastMessageReceived == null) {
return 0;
}
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(Duration(days: 2));
if (contact.lastMessageSend!.isBefore(twoDaysAgo) &&
contact.lastMessageReceived!.isBefore(twoDaysAgo)) {
return contact.flameCounter;
} else {
return 0;
}
}

View file

@ -0,0 +1,113 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
part 'database.g.dart';
// You can then create a database class that includes this table
@DriftDatabase(tables: [Contacts, Messages])
class TwonlyDatabase extends _$TwonlyDatabase {
TwonlyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'twonly_main_db',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
// ------------
Stream<List<Message>> watchMessageNotOpened(int userId) {
return (select(messages)
..where((t) => t.openedAt.isNull() & t.contactId.equals(userId)))
.watch();
}
Stream<Message?> watchLastMessage(int userId) {
return (select(messages)
..where((t) => t.contactId.equals(userId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)])
..limit(1))
.watchSingleOrNull();
}
// ------------
Future<int> insertContact(ContactsCompanion contact) {
return into(contacts).insert(contact);
}
SingleOrNullSelectable<Contact> getContactByUserId(int userId) {
return select(contacts)..where((t) => t.userId.equals(userId));
}
// Stream<int> getMaxTotalMediaCounter() {
// return customSelect(
// 'SELECT MAX(totalMediaCounter) AS maxTotal FROM contacts',
// readsFrom: {contacts},
// ).watchSingle().asyncMap((result) {
// return result.read<int>('maxTotal');
// });
// }
Future deleteContactByUserId(int userId) {
return (delete(contacts)..where((t) => t.userId.equals(userId))).go();
}
Future updateContact(int userId, ContactsCompanion updatedValues) {
return (update(contacts)..where((c) => c.userId.equals(userId)))
.write(updatedValues);
}
Stream<List<Contact>> watchNotAcceptedContacts() {
return (select(contacts)..where((t) => t.accepted.equals(false))).watch();
}
Stream<List<Contact>> watchContactsForChatList() {
return (select(contacts)
..where((t) => t.accepted.equals(true) & t.blocked.equals(false))
..orderBy([(t) => OrderingTerm.asc(t.lastMessage)]))
.watch();
}
Stream<int?> watchContactsBlocked() {
final count = contacts.blocked.count(distinct: true);
final query = selectOnly(contacts)..where(contacts.blocked.equals(true));
query.addColumns([count]);
return query.map((row) => row.read(count)).watchSingle();
}
Stream<int?> watchContactsRequested() {
final count = contacts.requested.count(distinct: true);
final query = selectOnly(contacts)..where(contacts.requested.equals(true));
query.addColumns([count]);
return query.map((row) => row.read(count)).watchSingle();
}
Stream<List<Contact>> watchAllContacts() {
return select(contacts).watch();
}
Stream<int> watchFlameCounter(int userId) {
return (select(contacts)
..where(
(u) =>
u.userId.equals(userId) &
u.lastMessageReceived.isNotNull() &
u.lastMessageSend.isNotNull(),
))
.watchSingle()
.asyncMap((contact) {
return getFlameCounterFromContact(contact);
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/model/json/message.dart';
enum DownloadState {
pending,
downloading,
downloaded,
}
class Messages extends Table {
IntColumn get contactId => integer().references(Contacts, #userId)();
IntColumn get messageId => integer().autoIncrement()();
IntColumn get messageOtherId => integer().nullable()();
IntColumn get responseToMessageId => integer().nullable()();
IntColumn get responseToOtherMessageId => integer().nullable()();
BoolColumn get acknowledgeByUser => boolean().withDefault(Constant(false))();
IntColumn get downloadState => intEnum<DownloadState>()();
BoolColumn get acknowledgeByServer =>
boolean().withDefault(Constant(false))();
TextColumn get kind => textEnum<MessageKind>()();
TextColumn get contentJson => text().nullable()();
DateTimeColumn get openedAt => dateTime().nullable()();
DateTimeColumn get sendAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
}

View file

@ -40,6 +40,9 @@
"chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!", "chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!",
"chatListViewSendFirstTwonly": "Sende dein erstes twonly!", "chatListViewSendFirstTwonly": "Sende dein erstes twonly!",
"chatListDetailInput": "Nachricht eingeben", "chatListDetailInput": "Nachricht eingeben",
"contextMenuVerifyUser": "Kontakt verifizieren",
"contextMenuOpenChat": "Chat öffnen",
"contextMenuSendImage": "Bild senden",
"messageSendState_Received": "Empfangen", "messageSendState_Received": "Empfangen",
"messageSendState_Opened": "Geöffnet", "messageSendState_Opened": "Geöffnet",
"messageSendState_Send": "Gesendet", "messageSendState_Send": "Gesendet",

View file

@ -40,6 +40,9 @@
"chatListViewSearchUserNameBtn": "Add your first twonly contact!", "chatListViewSearchUserNameBtn": "Add your first twonly contact!",
"chatListViewSendFirstTwonly": "Send your first twonly!", "chatListViewSendFirstTwonly": "Send your first twonly!",
"chatListDetailInput": "Type a message", "chatListDetailInput": "Type a message",
"contextMenuVerifyUser": "Verify user",
"contextMenuOpenChat": "Open chat",
"contextMenuSendImage": "Send image",
"mediaViewerAuthReason": "Please authenticate to see this twonly!", "mediaViewerAuthReason": "Please authenticate to see this twonly!",
"messageSendState_Received": "Received", "messageSendState_Received": "Received",
"messageSendState_Opened": "Opened", "messageSendState_Opened": "Opened",

View file

@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
enum MessageKind { enum MessageKind {
textMessage, textMessage,
image, media,
video,
contactRequest, contactRequest,
rejectRequest, rejectRequest,
acceptRequest, acceptRequest,
@ -11,63 +10,13 @@ enum MessageKind {
ack ack
} }
extension MessageKindExtension on MessageKind { Color getMessageColorFromType(MessageJson msg, Color primary) {
String get name => toString().split('.').last;
static MessageKind fromString(String name) {
return MessageKind.values.firstWhere((e) => e.name == name);
}
int get index => this.index;
static MessageKind fromIndex(int index) {
return MessageKind.values[index];
}
}
// TODO: use message as base class, remove kind and flatten content
class Message {
final MessageKind kind;
final MessageContent content;
final int? messageId;
DateTime timestamp;
Message(
{required this.kind,
this.messageId,
required this.content,
required this.timestamp});
@override
String toString() {
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
}
static Message fromJson(Map<String, dynamic> json) => Message(
kind: MessageKindExtension.fromString(json["kind"]),
messageId: (json['messageId'] as num?)?.toInt(),
content:
MessageContent.fromJson(json['content'] as Map<String, dynamic>),
timestamp: DateTime.parse(json['timestamp'] as String),
);
Map<String, dynamic> toJson() => <String, dynamic>{
'kind': kind.name,
'content': content.toJson(),
'messageId': messageId,
'timestamp': timestamp.toIso8601String(),
};
}
class MessageContent {
MessageContent();
Color getColor(Color primary) {
Color color; Color color;
if (this is TextMessageContent) {
final content = msg.content;
if (content is TextMessageContent) {
color = Colors.lightBlue; color = Colors.lightBlue;
} else { } else {
final content = this;
if (content is MediaMessageContent) { if (content is MediaMessageContent) {
if (content.isRealTwonly) { if (content.isRealTwonly) {
color = primary; color = primary;
@ -83,16 +32,64 @@ class MessageContent {
} }
} }
return color; return color;
}
extension MessageKindExtension on MessageKind {
String get name => toString().split('.').last;
static MessageKind fromString(String name) {
return MessageKind.values.firstWhere((e) => e.name == name);
}
}
class MessageJson {
final MessageKind kind;
final MessageContent? content;
final int? messageId;
DateTime timestamp;
MessageJson(
{required this.kind,
this.messageId,
required this.content,
required this.timestamp});
@override
String toString() {
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
} }
static MessageContent fromJson(Map json) { static MessageJson fromJson(Map<String, dynamic> json) {
switch (json['type']) { final kind = MessageKindExtension.fromString(json["kind"]);
case 'MediaMessageContent':
return MessageJson(
kind: kind,
messageId: (json['messageId'] as num?)?.toInt(),
content: MessageContent.fromJson(
kind, json['content'] as Map<String, dynamic>),
timestamp: DateTime.parse(json['timestamp'] as String),
);
}
Map<String, dynamic> toJson() => <String, dynamic>{
'kind': kind.name,
'content': content?.toJson(),
'messageId': messageId,
'timestamp': timestamp.toIso8601String(),
};
}
class MessageContent {
MessageContent();
static MessageContent? fromJson(MessageKind kind, Map json) {
switch (kind) {
case MessageKind.media:
return MediaMessageContent.fromJson(json); return MediaMessageContent.fromJson(json);
case 'TextMessageContent': case MessageKind.textMessage:
return TextMessageContent.fromJson(json); return TextMessageContent.fromJson(json);
default: default:
return MessageContent(); return null;
} }
} }
@ -125,7 +122,6 @@ class MediaMessageContent extends MessageContent {
@override @override
Map toJson() { Map toJson() {
return { return {
'type': 'MediaMessageContent',
'downloadToken': downloadToken, 'downloadToken': downloadToken,
'isRealTwonly': isRealTwonly, 'isRealTwonly': isRealTwonly,
'maxShowTime': maxShowTime, 'maxShowTime': maxShowTime,
@ -146,7 +142,6 @@ class TextMessageContent extends MessageContent {
@override @override
Map toJson() { Map toJson() {
return { return {
'type': 'TextMessageContent',
'text': text, 'text': text,
}; };
} }

View file

@ -1,5 +1,4 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fixnum/fixnum.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:twonly/src/utils/json.dart'; import 'package:twonly/src/utils/json.dart';
part 'signal_identity.g.dart'; part 'signal_identity.g.dart';
@ -9,8 +8,7 @@ class SignalIdentity {
const SignalIdentity( const SignalIdentity(
{required this.identityKeyPairU8List, required this.registrationId}); {required this.identityKeyPairU8List, required this.registrationId});
@Int64Converter() final int registrationId;
final Int64 registrationId;
@Uint8ListConverter() @Uint8ListConverter()
final Uint8List identityKeyPairU8List; final Uint8List identityKeyPairU8List;

View file

@ -10,13 +10,12 @@ SignalIdentity _$SignalIdentityFromJson(Map<String, dynamic> json) =>
SignalIdentity( SignalIdentity(
identityKeyPairU8List: const Uint8ListConverter() identityKeyPairU8List: const Uint8ListConverter()
.fromJson(json['identityKeyPairU8List'] as String), .fromJson(json['identityKeyPairU8List'] as String),
registrationId: registrationId: (json['registrationId'] as num).toInt(),
const Int64Converter().fromJson(json['registrationId'] as String),
); );
Map<String, dynamic> _$SignalIdentityToJson(SignalIdentity instance) => Map<String, dynamic> _$SignalIdentityToJson(SignalIdentity instance) =>
<String, dynamic>{ <String, dynamic>{
'registrationId': const Int64Converter().toJson(instance.registrationId), 'registrationId': instance.registrationId,
'identityKeyPairU8List': 'identityKeyPairU8List':
const Uint8ListConverter().toJson(instance.identityKeyPairU8List), const Uint8ListConverter().toJson(instance.identityKeyPairU8List),
}; };

View file

@ -1,6 +1,4 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:fixnum/fixnum.dart';
import 'package:twonly/src/utils/json.dart';
part 'user_data.g.dart'; part 'user_data.g.dart';
@JsonSerializable() @JsonSerializable()
@ -12,8 +10,7 @@ class UserData {
final String username; final String username;
final String displayName; final String displayName;
@Int64Converter() final int userId;
final Int64 userId;
factory UserData.fromJson(Map<String, dynamic> json) => factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json); _$UserDataFromJson(json);

View file

@ -7,7 +7,7 @@ part of 'user_data.dart';
// ************************************************************************** // **************************************************************************
UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData( UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
userId: const Int64Converter().fromJson(json['userId'] as String), userId: (json['userId'] as num).toInt(),
username: json['username'] as String, username: json['username'] as String,
displayName: json['displayName'] as String, displayName: json['displayName'] as String,
); );
@ -15,5 +15,5 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username, 'username': instance.username,
'displayName': instance.displayName, 'displayName': instance.displayName,
'userId': const Int64Converter().toJson(instance.userId), 'userId': instance.userId,
}; };

View file

@ -7,9 +7,9 @@ import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart'; import 'package:twonly/src/app.dart';
import 'package:twonly/src/model/contacts_model.dart'; import '../../../../.blocked/archives/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart'; import '../../../../.blocked/archives/messages_model.dart';
import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -30,7 +30,7 @@ Future tryTransmitMessages() async {
Uint8List? bytes = box.get("retransmit-$msgId-textmessage"); Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
if (bytes != null) { if (bytes != null) {
Result resp = await apiProvider.sendTextMessage( Result resp = await apiProvider.sendTextMessage(
Int64(retransmit[i].otherUserId), retransmit[i].otherUserId,
bytes, bytes,
); );
@ -46,7 +46,7 @@ Future tryTransmitMessages() async {
if (encryptedMedia != null) { if (encryptedMedia != null) {
final content = retransmit[i].messageContent; final content = retransmit[i].messageContent;
if (content is MediaMessageContent) { if (content is MediaMessageContent) {
uploadMediaFile(msgId, Int64(retransmit[i].otherUserId), encryptedMedia, uploadMediaFile(msgId, retransmit[i].otherUserId, encryptedMedia,
content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt); content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt);
} }
} }
@ -54,7 +54,7 @@ Future tryTransmitMessages() async {
} }
// this functions ensures that the message is received by the server and in case of errors will try again later // this functions ensures that the message is received by the server and in case of errors will try again later
Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async { Future<Result> encryptAndSendMessage(int userId, Message msg) async {
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId); Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
if (bytes == null) { if (bytes == null) {
@ -79,7 +79,7 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
return resp; return resp;
} }
Future sendTextMessage(Int64 target, String message) async { Future sendTextMessage(int target, String message) async {
MessageContent content = TextMessageContent(text: message); MessageContent content = TextMessageContent(text: message);
DateTime messageSendAt = DateTime.now(); DateTime messageSendAt = DateTime.now();
@ -105,7 +105,7 @@ Future sendTextMessage(Int64 target, String message) async {
// this will send the media file and ensures retransmission when errors occur // this will send the media file and ensures retransmission when errors occur
Future uploadMediaFile( Future uploadMediaFile(
int messageId, int messageId,
Int64 target, int target,
Uint8List encryptedMedia, Uint8List encryptedMedia,
bool isRealTwonly, bool isRealTwonly,
int maxShowTime, int maxShowTime,
@ -179,7 +179,7 @@ Future uploadMediaFile(
} }
class SendImage { class SendImage {
final Int64 userId; final int userId;
final Uint8List imageBytes; final Uint8List imageBytes;
final bool isRealTwonly; final bool isRealTwonly;
final int maxShowTime; final int maxShowTime;
@ -231,7 +231,7 @@ class SendImage {
} }
Future sendImage( Future sendImage(
List<Int64> userIds, List<int> userIds,
Uint8List imageBytes, Uint8List imageBytes,
bool isRealTwonly, bool isRealTwonly,
int maxShowTime, int maxShowTime,

View file

@ -5,9 +5,9 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart'; import 'package:twonly/src/app.dart';
import 'package:twonly/src/model/contacts_model.dart'; import '../../../../.blocked/archives/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart'; import '../../../../.blocked/archives/messages_model.dart';
import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client; import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client;
import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; import 'package:twonly/src/proto/api/client_to_server.pbserver.dart';
import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
@ -102,7 +102,7 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
int? fromUserId = box.get("${data.uploadToken}_fromUserId"); int? fromUserId = box.get("${data.uploadToken}_fromUserId");
if (fromUserId != null) { if (fromUserId != null) {
Uint8List? rawBytes = Uint8List? rawBytes =
await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId)); await SignalHelper.decryptBytes(downloadedBytes, fromUserId);
if (rawBytes != null) { if (rawBytes != null) {
box.put("${data.uploadToken}_downloaded", rawBytes); box.put("${data.uploadToken}_downloaded", rawBytes);

View file

@ -256,7 +256,7 @@ class ApiProvider {
var open = Handshake_OpenSession() var open = Handshake_OpenSession()
..response = signature ..response = signature
..userId = userData.userId; ..userId = Int64(userData.userId);
var opensession = Handshake()..opensession = open; var opensession = Handshake()..opensession = open;
@ -304,8 +304,8 @@ class ApiProvider {
return await _sendRequestV0(req); return await _sendRequestV0(req);
} }
Future<Result> getUsername(Int64 userId) async { Future<Result> getUsername(int userId) async {
var get = ApplicationData_GetUserById()..userId = userId; var get = ApplicationData_GetUserById()..userId = Int64(userId);
var appData = ApplicationData()..getuserbyid = get; var appData = ApplicationData()..getuserbyid = get;
var req = createClientToServerFromApplicationData(appData); var req = createClientToServerFromApplicationData(appData);
return await _sendRequestV0(req); return await _sendRequestV0(req);
@ -353,9 +353,9 @@ class ApiProvider {
return await _sendRequestV0(req); return await _sendRequestV0(req);
} }
Future<Result> sendTextMessage(Int64 target, Uint8List msg) async { Future<Result> sendTextMessage(int target, Uint8List msg) async {
var testMessage = ApplicationData_TextMessage() var testMessage = ApplicationData_TextMessage()
..userId = target ..userId = Int64(target)
..body = msg; ..body = msg;
var appData = ApplicationData()..textmessage = testMessage; var appData = ApplicationData()..textmessage = testMessage;

View file

@ -1,33 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/messages_model.dart';
// This provider will update the UI on changes in the contact list
class ContactChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
List<Contact> _allContacts = [];
final Map<int, DbMessage> _lastMessagesGroupedByUser = <int, DbMessage>{};
int get newContactRequests => _allContacts
.where((contact) => !contact.accepted && contact.requested)
.length;
List<Contact> get allContacts => _allContacts;
void update() async {
_allContacts = await DbContacts.getUsers();
for (Contact contact in _allContacts) {
DbMessage? last = await DbMessages.getLastMessagesForPreviewForUser(
contact.userId.toInt());
if (last != null) {
_lastMessagesGroupedByUser[last.otherUserId] = last;
}
}
notifyListeners();
}
/// Makes `Counter` readable inside the devtools by listing all of its properties
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('count', newContactRequests));
}
}

View file

@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/identity_key_store_model.dart'; import 'package:twonly/src/model/identity_key_store_model.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/model/pre_key_model.dart'; import 'package:twonly/src/model/pre_key_model.dart';
import 'package:twonly/src/model/sender_key_store_model.dart'; import 'package:twonly/src/model/sender_key_store_model.dart';
import 'package:twonly/src/model/session_store_model.dart'; import 'package:twonly/src/model/session_store_model.dart';
@ -55,8 +53,6 @@ class DbProvider {
await DbSignalPreKeyStore.setupDatabaseTable(db); await DbSignalPreKeyStore.setupDatabaseTable(db);
await DbSignalSenderKeyStore.setupDatabaseTable(db); await DbSignalSenderKeyStore.setupDatabaseTable(db);
await DbSignalIdentityKeyStore.setupDatabaseTable(db); await DbSignalIdentityKeyStore.setupDatabaseTable(db);
await DbContacts.setupDatabaseTable(db);
await DbMessages.setupDatabaseTable(db);
} }
Future open() async { Future open() async {

View file

@ -1,20 +0,0 @@
import 'dart:collection';
import 'package:flutter/foundation.dart';
class DownloadChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
final HashSet<String> _currentlyDownloading = HashSet<String>();
HashSet<String> get currentlyDownloading => _currentlyDownloading;
void update(List<int> token, bool add) {
debugPrint("Downloading: $add : $token");
if (add) {
_currentlyDownloading.add(token.toString());
} else {
_currentlyDownloading.remove(token.toString());
}
debugPrint("Downloading: $add : ${_currentlyDownloading.toList()}");
notifyListeners();
}
}

View file

@ -1,76 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/utils/misc.dart';
/// This provider does always contains the latest messages send or received
/// for every contact.
class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
final Map<int, DbMessage> _lastMessage = <int, DbMessage>{};
final Map<int, List<DbMessage>> _allMessagesFromUser =
<int, List<DbMessage>>{};
final Map<int, int> _changeCounter = <int, int>{};
final Map<int, int> _flamesCounter = <int, int>{};
Map<int, DbMessage> get lastMessage => _lastMessage;
Map<int, List<DbMessage>> get allMessagesFromUser => _allMessagesFromUser;
Map<int, int> get changeCounter => _changeCounter;
Map<int, int> get flamesCounter => _flamesCounter;
Future updateLastMessageFor(int targetUserId, int? messageId) async {
DbMessage? last =
await DbMessages.getLastMessagesForPreviewForUser(targetUserId);
if (last != null) {
_lastMessage[last.otherUserId] = last;
}
flamesCounter[targetUserId] = await getFlamesForOtherUser(targetUserId);
// notifyListeners();
if (messageId == null || _allMessagesFromUser[targetUserId] == null) {
loadMessagesForUser(targetUserId, force: true);
} else {
DbMessage? msg = await DbMessages.getMessageById(messageId);
if (msg != null) {
int index = _allMessagesFromUser[targetUserId]!
.indexWhere((x) => x.messageId == messageId);
if (index == -1) {
print("should be indexed by time!!");
_allMessagesFromUser[targetUserId]!.insert(0, msg);
// reload all messages but async
loadMessagesForUser(targetUserId, force: true);
} else {
_allMessagesFromUser[targetUserId]![index] = msg;
}
}
}
notifyListeners();
}
Future loadMessagesForUser(int targetUserId, {bool force = false}) async {
if (!force && _allMessagesFromUser[targetUserId] != null) return;
_allMessagesFromUser[targetUserId] =
await DbMessages.getAllMessagesForUser(targetUserId);
notifyListeners();
}
void init({bool afterPaused = false}) async {
// load everything from the database
List<Contact> allContacts = await DbContacts.getUsers();
for (Contact contact in allContacts) {
DbMessage? last = await DbMessages.getLastMessagesForPreviewForUser(
contact.userId.toInt());
if (last != null) {
_lastMessage[last.otherUserId] = last;
}
flamesCounter[contact.userId.toInt()] =
await getFlamesForOtherUser(contact.userId.toInt());
}
notifyListeners();
if (afterPaused) {
for (int targetUserId in _allMessagesFromUser.keys) {
loadMessagesForUser(targetUserId, force: true);
}
}
}
}

View file

@ -3,8 +3,8 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart'; import 'package:twonly/src/app.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import '../../firebase_options.dart'; import '../../firebase_options.dart';
@ -65,33 +65,26 @@ Future initFCMService() async {
}); });
} }
late TwonlyDatabase bgTwonlyDB;
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// If you're going to use other Firebase services in the background, such as Firestore,
// make sure you call `initializeApp` before using other Firebase services.
// Wenn Tasks länger als 30 Sekunden ausgeführt werden, wird der Prozess möglicherweise automatisch vom Gerät beendet. // Wenn Tasks länger als 30 Sekunden ausgeführt werden, wird der Prozess möglicherweise automatisch vom Gerät beendet.
// -> offer backend via http? // -> offer backend via http?
print("Handling a background message: ${message.messageId}"); Logger("firebase-background")
.shout('Handling a background message: ${message.messageId}');
bool gotMessage = false; bgTwonlyDB = TwonlyDatabase();
globalCallBackOnMessageChange = (a, b) async {
gotMessage = true;
print("Got message can exit");
};
dbProvider = DbProvider();
await dbProvider.ready;
apiProvider = ApiProvider(); apiProvider = ApiProvider();
await apiProvider.connect(); await apiProvider.connect();
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
while (!gotMessage) { while (true) {
if (stopwatch.elapsed >= Duration(seconds: 20)) { if (stopwatch.elapsed >= Duration(seconds: 20)) {
Logger("firebase-background").shout('Timeout reached. Exiting the loop.'); Logger("firebase-background").shout('Exiting background handler');
break; // Exit the loop if the timeout is reached. break;
} }
await Future.delayed(Duration(milliseconds: 10)); await Future.delayed(Duration(milliseconds: 10));
} }

View file

@ -1,10 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/model/json/message.dart' as my; import 'package:twonly/src/model/json/message.dart' as my;
/// Streams are created so that app can respond to notification-related events /// Streams are created so that app can respond to notification-related events
@ -143,9 +142,9 @@ String getPushNotificationText(String key, String userName) {
if (systemLanguage.contains("de")) { if (systemLanguage.contains("de")) {
pushNotificationText = { pushNotificationText = {
"newTextMessage": "%userName% hat dir eine Nachricht gesendet.", "newTextMessage": "%userName% hat dir eine Nachricht gesendet.",
"newTwonly": "%userName% hat dir einen twonly gesendet.", "newTwonly": "%userName% hat dir ein twonly gesendet.",
"newVideo": "%userName% hat dir eine Video gesendet.", "newVideo": "%userName% hat dir ein Video gesendet.",
"newImage": "%userName% hat dir eine Bild gesendet.", "newImage": "%userName% hat dir ein Bild gesendet.",
"contactRequest": "%userName% möchte sich mir dir vernetzen.", "contactRequest": "%userName% möchte sich mir dir vernetzen.",
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.", "acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
}; };
@ -166,7 +165,10 @@ String getPushNotificationText(String key, String userName) {
Future localPushNotificationNewMessage( Future localPushNotificationNewMessage(
int fromUserId, my.Message message, int messageId) async { int fromUserId, my.Message message, int messageId) async {
Contact? user = await DbContacts.getUserById(fromUserId); Contact? user = await TwonlyDatabase.provider
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) return; if (user == null) return;
String msg = ""; String msg = "";

View file

@ -1,65 +0,0 @@
// The callback function should always be a top-level or static function.
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
@pragma('vm:entry-point')
void startCallback() {
FlutterForegroundTask.setTaskHandler(WebsocketForegroundTask());
}
class WebsocketForegroundTask extends TaskHandler {
// Called when the task is started.
@override
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
print('onStart(starter: ${starter.name})');
dbProvider = DbProvider();
await dbProvider.ready;
apiProvider = ApiProvider();
apiProvider.connect();
}
// Called based on the eventAction set in ForegroundTaskOptions.
@override
void onRepeatEvent(DateTime timestamp) {
// Send data to main isolate.
final Map<String, dynamic> data = {
"timestampMillis": timestamp.millisecondsSinceEpoch,
};
FlutterForegroundTask.sendDataToMain(data);
}
// Called when the task is destroyed.
@override
Future<void> onDestroy(DateTime timestamp) async {
await apiProvider.close(() {});
print('onDestroy');
}
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.
@override
void onReceiveData(Object data) {
apiProvider.close(() {});
print('onReceiveData: $data');
}
// Called when the notification button is pressed.
@override
void onNotificationButtonPressed(String id) {
print('onNotificationButtonPressed: $id');
}
// Called when the notification itself is pressed.
@override
void onNotificationPressed() {
apiProvider.close(() {});
}
// Called when the notification itself is dismissed.
@override
void onNotificationDismissed() {
print('onNotificationDismissed');
}
}

View file

@ -1,22 +1,7 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:fixnum/fixnum.dart';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
class Int64Converter implements JsonConverter<Int64, String> {
const Int64Converter();
@override
Int64 fromJson(String json) {
return Int64.parseInt(json);
}
@override
String toJson(Int64 object) {
return object.toString();
}
}
class Uint8ListConverter implements JsonConverter<Uint8List, String> { class Uint8ListConverter implements JsonConverter<Uint8List, String> {
const Uint8ListConverter(); const Uint8ListConverter();
@override @override

View file

@ -9,13 +9,15 @@ import 'package:local_auth/local_auth.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:provider/provider.dart';
import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension LocalizationExtension on BuildContext { extension ShortCutsExtension on BuildContext {
AppLocalizations get lang => AppLocalizations.of(this)!; AppLocalizations get lang => AppLocalizations.of(this)!;
TwonlyDatabase get db => Provider.of<TwonlyDatabase>(this);
} }
// Function to check if a column exists // Function to check if a column exists
@ -148,50 +150,48 @@ Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
return result; return result;
} }
int getFlameCounter(List<DateTime> dates) { // int getFlameCounter(List<DateTime> dates) {
if (dates.isEmpty) return 0; // if (dates.isEmpty) return 0;
int flamesCount = 0; // int flamesCount = 0;
DateTime lastFlameCount = DateTime.now(); // DateTime lastFlameCount = DateTime.now();
if (calculateTimeDifference(dates[0], lastFlameCount).inDays == 0) { // if (calculateTimeDifference(dates[0], lastFlameCount).inDays == 0) {
flamesCount = 1; // flamesCount = 1;
lastFlameCount = dates[0]; // lastFlameCount = dates[0];
} // }
// print(dates[0]); // // print(dates[0]);
for (int i = 1; i < dates.length; i++) { // for (int i = 1; i < dates.length; i++) {
// print( // // print(
// "${dates[i]} ${dates[i].difference(dates[i - 1]).inDays} ${dates[i].difference(lastFlameCount).inDays}"); // // "${dates[i]} ${dates[i].difference(dates[i - 1]).inDays} ${dates[i].difference(lastFlameCount).inDays}");
if (calculateTimeDifference(dates[i], dates[i - 1]).inDays == 0) { // if (calculateTimeDifference(dates[i], dates[i - 1]).inDays == 0) {
if (lastFlameCount.difference(dates[i]).inDays == 1) { // if (lastFlameCount.difference(dates[i]).inDays == 1) {
flamesCount++; // flamesCount++;
lastFlameCount = dates[i]; // lastFlameCount = dates[i];
} // }
} else { // } else {
break; // Stop counting if there's a break in the sequence // break; // Stop counting if there's a break in the sequence
} // }
} // }
return flamesCount; // return flamesCount;
} // }
Future<int> getFlamesForOtherUser(int otherUserId) async { // Future<int> getFlamesForOtherUser(int otherUserId) async {
List<(DateTime, int?)> dates = await DbMessages.getMessageDates(otherUserId); // List<(DateTime, int?)> dates = await DbMessages.getMessageDates(otherUserId);
// print("Dates ${dates.length}"); // // print("Dates ${dates.length}");
if (dates.isEmpty) return 0; // if (dates.isEmpty) return 0;
List<DateTime> received = // List<DateTime> received =
dates.where((x) => x.$2 != null).map((x) => x.$1).toList(); // dates.where((x) => x.$2 != null).map((x) => x.$1).toList();
List<DateTime> send = // List<DateTime> send =
dates.where((x) => x.$2 == null).map((x) => x.$1).toList(); // dates.where((x) => x.$2 == null).map((x) => x.$1).toList();
// print("Received ${received.length} and send ${send.length}"); // int a = getFlameCounter(received);
// int b = getFlameCounter(send);
int a = getFlameCounter(received); // // print("Received $a and send $b");
int b = getFlameCounter(send); // return min(a, b);
// print("Received $a and send $b"); // }
return min(a, b);
}
Duration calculateTimeDifference(DateTime now, DateTime startTime) { Duration calculateTimeDifference(DateTime now, DateTime startTime) {
// Get the timezone offsets // Get the timezone offsets

View file

@ -27,7 +27,7 @@ Future<ECPrivateKey?> getPrivateKey() async {
} }
Future<bool> addNewContact(Response_UserData userData) async { Future<bool> addNewContact(Response_UserData userData) async {
final Int64 userId = userData.userId; final int userId = userData.userId.toInt();
SignalProtocolAddress targetAddress = SignalProtocolAddress targetAddress =
SignalProtocolAddress(userId.toString(), defaultDeviceId); SignalProtocolAddress(userId.toString(), defaultDeviceId);
@ -141,13 +141,14 @@ Future createIfNotExistsSignalIdentity() async {
final storedSignalIdentity = SignalIdentity( final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(), identityKeyPairU8List: identityKeyPair.serialize(),
registrationId: Int64(registrationId)); registrationId: registrationId,
);
await storage.write( await storage.write(
key: "signal_identity", value: jsonEncode(storedSignalIdentity)); key: "signal_identity", value: jsonEncode(storedSignalIdentity));
} }
Future<Fingerprint?> generateSessionFingerPrint(Int64 target) async { Future<Fingerprint?> generateSessionFingerPrint(int target) async {
ConnectSignalProtocolStore? signalStore = await getSignalStore(); ConnectSignalProtocolStore? signalStore = await getSignalStore();
UserData? user = await getUser(); UserData? user = await getUser();
if (signalStore == null || user == null) return null; if (signalStore == null || user == null) return null;
@ -215,7 +216,7 @@ Future<Uint8List?> encryptBytes(Uint8List bytes, Int64 target) async {
} }
} }
Future<Uint8List?> decryptBytes(Uint8List bytes, Int64 target) async { Future<Uint8List?> decryptBytes(Uint8List bytes, int target) async {
try { try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!; ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
@ -247,7 +248,7 @@ Future<Uint8List?> decryptBytes(Uint8List bytes, Int64 target) async {
} }
} }
Future<Uint8List?> encryptMessage(Message msg, Int64 target) async { Future<Uint8List?> encryptMessage(Message msg, int target) async {
try { try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!; ConnectSignalProtocolStore signalStore = (await getSignalStore())!;

View file

@ -375,7 +375,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (sendNextMediaToUserId != null) { if (sendNextMediaToUserId != null) {
Uint8List? imageBytes = await getMergedImage(); Uint8List? imageBytes = await getMergedImage();
sendImage( sendImage(
[Int64(sendNextMediaToUserId)], [sendNextMediaToUserId],
imageBytes!, imageBytes!,
_isRealTwonly, _isRealTwonly,
_maxShowTime, _maxShowTime,

View file

@ -9,7 +9,7 @@ import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/components/verified_shield.dart';
import 'package:twonly/src/model/contacts_model.dart'; import '../../../../.blocked/archives/contacts_model.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/send_next_media_to.dart';

View file

@ -6,9 +6,9 @@ import 'package:twonly/src/components/animate_icon.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/message_send_state_icon.dart'; import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/components/verified_shield.dart';
import 'package:twonly/src/model/contacts_model.dart'; import '../../../../.blocked/archives/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart'; import '../../../../.blocked/archives/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/download_change_provider.dart'; import 'package:twonly/src/providers/download_change_provider.dart';

View file

@ -7,13 +7,11 @@ import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/message_send_state_icon.dart'; import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/components/notification_badge.dart'; import 'package:twonly/src/components/notification_badge.dart';
import 'package:twonly/src/components/user_context_menu.dart'; import 'package:twonly/src/components/user_context_menu.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_item_details_view.dart'; import 'package:twonly/src/views/chats/chat_item_details_view.dart';
@ -34,29 +32,7 @@ class ChatListView extends StatefulWidget {
class _ChatListViewState extends State<ChatListView> { class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Map<int, DbMessage> lastMessages = Stream<List<Contact>> contacts = context.db.watchContactsForChatList();
context.watch<MessagesChangeProvider>().lastMessage;
List<Contact> allUsers = context
.watch<ContactChangeProvider>()
.allContacts
.where((c) => c.accepted)
.toList();
allUsers.sort((b, a) {
DbMessage? msgA = lastMessages[a.userId.toInt()];
DbMessage? msgB = lastMessages[b.userId.toInt()];
if (msgA == null) return 1;
if (msgB == null) return -1;
return msgA.sendAt.compareTo(msgB.sendAt);
});
int maxTotalMediaCounter = 0;
if (allUsers.isNotEmpty) {
maxTotalMediaCounter = allUsers
.map((x) => x.totalMediaCounter)
.reduce((a, b) => a > b ? a : b);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -73,11 +49,15 @@ class _ChatListViewState extends State<ChatListView> {
), ),
// title: // title:
actions: [ actions: [
NotificationBadge( StreamBuilder(
count: context stream: context.db.watchContactsRequested(),
.watch<ContactChangeProvider>() builder: (context, snapshot) {
.newContactRequests var count = 0;
.toString(), if (snapshot.hasData && snapshot.data != null) {
count = snapshot.data!;
}
return NotificationBadge(
count: count.toString(),
child: IconButton( child: IconButton(
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18), icon: FaIcon(FontAwesomeIcons.userPlus, size: 18),
onPressed: () { onPressed: () {
@ -89,6 +69,8 @@ class _ChatListViewState extends State<ChatListView> {
); );
}, },
), ),
);
},
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
@ -103,8 +85,16 @@ class _ChatListViewState extends State<ChatListView> {
) )
], ],
), ),
body: (allUsers.isEmpty) body: StreamBuilder(
? Center( stream: contacts,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contacts = snapshot.data!;
if (contacts.isEmpty) {
return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: OutlinedButton.icon( child: OutlinedButton.icon(
@ -113,39 +103,42 @@ class _ChatListViewState extends State<ChatListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SearchUsernameView(), builder: (context) => SearchUsernameView()));
),
);
}, },
label: Text(context.lang.chatListViewSearchUserNameBtn)), label: Text(context.lang.chatListViewSearchUserNameBtn)),
), ),
) );
: ListView.builder( }
int maxTotalMediaCounter = 0;
if (contacts.isNotEmpty) {
maxTotalMediaCounter = contacts
.map((x) => x.totalMediaCounter)
.reduce((a, b) => a > b ? a : b);
}
return ListView.builder(
restorationId: 'chat_list_view', restorationId: 'chat_list_view',
itemCount: allUsers.length, itemCount: contacts.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final user = allUsers[index]; final user = contacts[index];
return UserListItem( return UserListItem(
user: user, user: user,
maxTotalMediaCounter: maxTotalMediaCounter, maxTotalMediaCounter: maxTotalMediaCounter,
lastMessage: lastMessages[user.userId.toInt()],
); );
}, },
),
); );
},
));
} }
} }
class UserListItem extends StatefulWidget { class UserListItem extends StatefulWidget {
final Contact user; final Contact user;
final DbMessage? lastMessage;
final int maxTotalMediaCounter; final int maxTotalMediaCounter;
const UserListItem( const UserListItem(
{super.key, {super.key, required this.user, required this.maxTotalMediaCounter});
required this.user,
required this.lastMessage,
required this.maxTotalMediaCounter});
@override @override
State<UserListItem> createState() => _UserListItem(); State<UserListItem> createState() => _UserListItem();
@ -154,37 +147,37 @@ class UserListItem extends StatefulWidget {
class _UserListItem extends State<UserListItem> { class _UserListItem extends State<UserListItem> {
int lastMessageInSeconds = 0; int lastMessageInSeconds = 0;
MessageSendState state = MessageSendState.send; MessageSendState state = MessageSendState.send;
bool isDownloading = false;
List<int> token = []; List<int> token = [];
Message? currentMessage;
Timer? updateTime; Timer? updateTime;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initAsync(); // initAsync();
lastUpdateTime(); lastUpdateTime();
} }
Future initAsync() async { // Future initAsync() async {
if (widget.lastMessage != null) { // if (currentMessage != null) {
if (!widget.lastMessage!.isDownloaded) { // if (currentMessage!.downloadState != DownloadState.downloading) {
final content = widget.lastMessage!.messageContent; // final content = widget.lastMessage!.messageContent;
if (content is MediaMessageContent) { // if (content is MediaMessageContent) {
tryDownloadMedia(widget.lastMessage!.messageId, // tryDownloadMedia(widget.lastMessage!.messageId,
widget.lastMessage!.otherUserId, content.downloadToken); // widget.lastMessage!.otherUserId, content.downloadToken);
} // }
} // }
} // }
} // }
void lastUpdateTime() { void lastUpdateTime() {
// Change the color every 200 milliseconds // Change the color every 200 milliseconds
updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) { updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) {
setState(() { setState(() {
if (widget.lastMessage != null) { if (currentMessage != null) {
lastMessageInSeconds = calculateTimeDifference( lastMessageInSeconds =
DateTime.now(), widget.lastMessage!.sendAt) calculateTimeDifference(DateTime.now(), currentMessage!.sendAt)
.inSeconds; .inSeconds;
} }
}); });
@ -199,35 +192,56 @@ class _UserListItem extends State<UserListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.lastMessage != null) { final notOpenedMessages =
state = widget.lastMessage!.getSendState(); context.db.watchMessageNotOpened(widget.user.userId);
final lastMessage = context.db.watchLastMessage(widget.user.userId);
final content = widget.lastMessage!.messageContent; // if (widget.lastMessage != null) {
// state = widget.lastMessage!.getSendState();
if (widget.lastMessage!.messageReceived && // final content = widget.lastMessage!.messageContent;
content is MediaMessageContent) {
token = content.downloadToken;
isDownloading = context
.watch<DownloadChangeProvider>()
.currentlyDownloading
.contains(token.toString());
}
}
int flameCounter = context // if (widget.lastMessage!.messageReceived &&
.watch<MessagesChangeProvider>() // content is MediaMessageContent) {
.flamesCounter[widget.user.userId.toInt()] ?? // token = content.downloadToken;
0; // isDownloading = context
// .watch<DownloadChangeProvider>()
// .currentlyDownloading
// .contains(token.toString());
// }
// }
int flameCounter = getFlameCounterFromContact(widget.user);
return UserContextMenu( return UserContextMenu(
user: widget.user, contact: widget.user,
child: ListTile( child: ListTile(
title: Text(widget.user.displayName), title: Text(getContactDisplayName(widget.user)),
subtitle: (widget.lastMessage == null) subtitle: StreamBuilder(
? Text(context.lang.chatsTapToSend) stream: lastMessage,
: Row( builder: (context, lastMessageSnapshot) {
if (!lastMessageSnapshot.hasData) {
return Container();
}
if (lastMessageSnapshot.data == null) {
return Text(context.lang.chatsTapToSend);
}
final lastMessage = lastMessageSnapshot.data!;
return StreamBuilder(
stream: notOpenedMessages,
builder: (context, notOpenedMessagesSnapshot) {
if (!lastMessageSnapshot.hasData) {
return Container();
}
var lastMessages = [lastMessage];
if (notOpenedMessagesSnapshot.data != null) {
lastMessages = notOpenedMessagesSnapshot.data!;
}
return Row(
children: [ children: [
MessageSendStateIcon(widget.lastMessage!), MessageSendStateIcon(lastMessages),
Text(""), Text(""),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
@ -242,29 +256,34 @@ class _UserListItem extends State<UserListItem> {
prefix: true, prefix: true,
), ),
], ],
);
},
);
},
), ),
leading: InitialsAvatar(displayName: widget.user.displayName), leading: InitialsAvatar(getContactDisplayName(widget.user)),
onTap: () { onTap: () {
if (widget.lastMessage == null) { if (currentMessage == null) {
context context
.read<SendNextMediaTo>() .read<SendNextMediaTo>()
.updateSendNextMediaTo(widget.user.userId.toInt()); .updateSendNextMediaTo(widget.user.userId.toInt());
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
return; return;
} }
if (isDownloading) return; Message msg = currentMessage!;
if (!widget.lastMessage!.isDownloaded) { if (msg.downloadState == DownloadState.downloading) {
tryDownloadMedia(widget.lastMessage!.messageId, return;
widget.lastMessage!.otherUserId, token, }
force: true); if (msg.downloadState == DownloadState.pending) {
tryDownloadMedia(msg.messageId, msg.contactId, token, force: true);
return; return;
} }
if (state == MessageSendState.received && if (state == MessageSendState.received &&
widget.lastMessage!.containsOtherMedia()) { msg.kind == MessageKind.media) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return MediaViewerView(widget.user, widget.lastMessage!); return MediaViewerView(widget.user, msg);
}), }),
); );
return; return;

View file

@ -7,9 +7,9 @@ import 'package:no_screenshot/no_screenshot.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/components/animate_icon.dart'; import 'package:twonly/src/components/animate_icon.dart';
import 'package:twonly/src/components/media_view_sizing.dart'; import 'package:twonly/src/components/media_view_sizing.dart';
import 'package:twonly/src/model/contacts_model.dart'; import '../../../../.blocked/archives/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart'; import '../../../../.blocked/archives/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/send_next_media_to.dart';

View file

@ -1,14 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/components/alert_dialog.dart'; import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
// ignore: library_prefixes // ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/signal.dart' as SignalHelper;
@ -31,18 +31,28 @@ class _SearchUsernameView extends State<SearchUsernameView> {
final res = await apiProvider.getUserData(searchUserName.text); final res = await apiProvider.getUserData(searchUserName.text);
if (res.isSuccess) { if (!context.mounted) {
bool added = await DbContacts.insertNewContact( return;
searchUserName.text, }
res.value.userdata.userId.toInt(),
false,
);
if (added) { if (res.isSuccess) {
final addUser = await showAlertDialog(
context, "User found", "Do you want to create a follow request?");
if (!addUser || !context.mounted) {
return;
}
int added = await context.db.insertContact(ContactsCompanion(
username: Value(searchUserName.text),
userId: Value(res.value.userdata.userId),
requested: Value(false),
));
if (added > 0) {
if (await SignalHelper.addNewContact(res.value.userdata)) { if (await SignalHelper.addNewContact(res.value.userdata)) {
encryptAndSendMessage( encryptAndSendMessage(
res.value.userdata.userId, res.value.userdata.userId,
Message( MessageJson(
kind: MessageKind.contactRequest, kind: MessageKind.contactRequest,
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(), content: MessageContent(),
@ -50,7 +60,7 @@ class _SearchUsernameView extends State<SearchUsernameView> {
); );
} }
} }
} else if (context.mounted) { } else {
showAlertDialog(context, context.lang.searchUsernameNotFound, showAlertDialog(context, context.lang.searchUsernameNotFound,
context.lang.searchUsernameNotFoundBody(searchUserName.text)); context.lang.searchUsernameNotFoundBody(searchUserName.text));
} }
@ -79,6 +89,8 @@ class _SearchUsernameView extends State<SearchUsernameView> {
); );
} }
Stream<List<Contact>> contacts = context.db.watchNotAcceptedContacts();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.searchUsernameTitle), title: Text(context.lang.searchUsernameTitle),
@ -108,14 +120,24 @@ class _SearchUsernameView extends State<SearchUsernameView> {
label: Text(context.lang.searchUsernameQrCodeBtn), label: Text(context.lang.searchUsernameQrCodeBtn),
), ),
SizedBox(height: 30), SizedBox(height: 30),
if (context StreamBuilder(
.watch<ContactChangeProvider>() stream: contacts,
.allContacts builder: (context, snapshot) {
.where((contact) => !contact.accepted) if (!snapshot.hasData || snapshot.data != null) {
.isNotEmpty) return Container();
HeadLineComponent(context.lang.searchUsernameNewFollowerTitle), }
final contacts = snapshot.data!;
if (contacts.isEmpty) {
return Container();
}
return Row(children: [
HeadLineComponent(
context.lang.searchUsernameNewFollowerTitle),
Expanded( Expanded(
child: ContactsListView(), child: ContactsListView(contacts),
)
]);
},
) )
], ],
), ),
@ -136,7 +158,9 @@ class _SearchUsernameView extends State<SearchUsernameView> {
} }
class ContactsListView extends StatefulWidget { class ContactsListView extends StatefulWidget {
const ContactsListView({super.key}); const ContactsListView(this.contacts, {super.key});
final List<Contact> contacts;
@override @override
State<ContactsListView> createState() => _ContactsListViewState(); State<ContactsListView> createState() => _ContactsListViewState();
@ -145,18 +169,14 @@ class ContactsListView extends StatefulWidget {
class _ContactsListViewState extends State<ContactsListView> { class _ContactsListViewState extends State<ContactsListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Contact> contacts = context
.read<ContactChangeProvider>()
.allContacts
.where((contact) => !contact.accepted)
.toList();
return ListView.builder( return ListView.builder(
itemCount: contacts.length, itemCount: widget.contacts.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final contact = contacts[index]; final contact = widget.contacts[index];
final displayName = getContactDisplayName(contact);
return ListTile( return ListTile(
title: Text(contact.displayName), title: Text(displayName),
leading: InitialsAvatar(displayName: contact.displayName), leading: InitialsAvatar(displayName),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -168,7 +188,8 @@ class _ContactsListViewState extends State<ContactsListView> {
icon: Icon(Icons.person_off_rounded, icon: Icon(Icons.person_off_rounded,
color: const Color.fromARGB(164, 244, 67, 54)), color: const Color.fromARGB(164, 244, 67, 54)),
onPressed: () async { onPressed: () async {
await DbContacts.blockUser(contact.userId.toInt()); final update = ContactsCompanion(blocked: Value(true));
await context.db.updateContact(contact.userId, update);
}, },
), ),
), ),
@ -177,10 +198,10 @@ class _ContactsListViewState extends State<ContactsListView> {
child: IconButton( child: IconButton(
icon: Icon(Icons.close, color: Colors.red), icon: Icon(Icons.close, color: Colors.red),
onPressed: () async { onPressed: () async {
await DbContacts.deleteUser(contact.userId.toInt()); await context.db.deleteContactByUserId(contact.userId);
encryptAndSendMessage( encryptAndSendMessage(
contact.userId, contact.userId,
Message( MessageJson(
kind: MessageKind.rejectRequest, kind: MessageKind.rejectRequest,
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(), content: MessageContent(),
@ -192,10 +213,11 @@ class _ContactsListViewState extends State<ContactsListView> {
IconButton( IconButton(
icon: Icon(Icons.check, color: Colors.green), icon: Icon(Icons.check, color: Colors.green),
onPressed: () async { onPressed: () async {
await DbContacts.acceptUser(contact.userId.toInt()); final update = ContactsCompanion(accepted: Value(true));
await context.db.updateContact(contact.userId, update);
encryptAndSendMessage( encryptAndSendMessage(
contact.userId, contact.userId,
Message( MessageJson(
kind: MessageKind.acceptRequest, kind: MessageKind.acceptRequest,
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(), content: MessageContent(),

View file

@ -1,12 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:drift/drift.dart' hide Column;
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:twonly/src/components/format_long_string.dart'; import 'package:twonly/src/components/format_long_string.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/signal.dart'; import 'package:twonly/src/utils/signal.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -36,10 +36,9 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Contact contact = context Stream<Contact?> contact = context.db
.watch<ContactChangeProvider>() .getContactByUserId(widget.contact.userId)
.allContacts .watchSingleOrNull();
.firstWhere((c) => c.userId == widget.contact.userId);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -92,13 +91,21 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
), ),
), ),
), ),
Padding( StreamBuilder(
stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30), padding: const EdgeInsets.symmetric(horizontal: 30),
child: Text( child: Text(
context.lang context.lang.contactVerifyNumberLongDesc(
.contactVerifyNumberLongDesc(contact.displayName), getContactDisplayName(snapshot.data!)),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
);
},
), ),
Padding( Padding(
padding: padding:
@ -125,23 +132,33 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
contact.verified StreamBuilder(
? OutlinedButton.icon( stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contact = snapshot.data!;
if (contact.verified) {
return OutlinedButton.icon(
onPressed: () { onPressed: () {
DbContacts.updateVerificationStatus( final update =
contact.userId.toInt(), false); ContactsCompanion(verified: Value(false));
context.db.updateContact(contact.userId, update);
}, },
label: Text( label: Text(
context.lang.contactVerifyNumberClearVerification), context.lang.contactVerifyNumberClearVerification),
) );
: FilledButton.icon( }
return FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.shieldHeart), icon: FaIcon(FontAwesomeIcons.shieldHeart),
onPressed: () { onPressed: () {
DbContacts.updateVerificationStatus( final update = ContactsCompanion(verified: Value(true));
contact.userId.toInt(), true); context.db.updateContact(contact.userId, update);
},
label: Text(context.lang.contactVerifyNumberMarkAsVerified),
);
}, },
label:
Text(context.lang.contactVerifyNumberMarkAsVerified),
), ),
], ],
), ),

View file

@ -1,14 +1,13 @@
import 'package:drift/drift.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/components/alert_dialog.dart'; import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/components/better_list_title.dart'; import 'package:twonly/src/components/better_list_title.dart';
import 'package:twonly/src/components/flame.dart'; import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/components/verified_shield.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_verify_view.dart'; import 'package:twonly/src/views/contact/contact_verify_view.dart';
@ -24,26 +23,27 @@ class ContactView extends StatefulWidget {
class _ContactViewState extends State<ContactView> { class _ContactViewState extends State<ContactView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Contact contact = context Stream<Contact?> contact =
.watch<ContactChangeProvider>() context.db.getContactByUserId(widget.userId).watchSingleOrNull();
.allContacts
.firstWhere((c) => c.userId == widget.userId);
int flameCounter = context
.watch<MessagesChangeProvider>()
.flamesCounter[contact.userId.toInt()] ??
0;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(""), title: Text(""),
), ),
body: ListView( body: StreamBuilder(
stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contact = snapshot.data!;
int flameCounter = getFlameCounterFromContact(contact);
return ListView(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: InitialsAvatar( child: InitialsAvatar(
displayName: contact.displayName, getContactDisplayName(contact),
fontSize: 30, fontSize: 30,
), ),
), ),
@ -54,7 +54,7 @@ class _ContactViewState extends State<ContactView> {
padding: EdgeInsets.only(right: 10), padding: EdgeInsets.only(right: 10),
child: VerifiedShield(contact)), child: VerifiedShield(contact)),
Text( Text(
contact.displayName, getContactDisplayName(contact),
style: TextStyle(fontSize: 20), style: TextStyle(fontSize: 20),
), ),
if (flameCounter > 0) if (flameCounter > 0)
@ -71,11 +71,12 @@ class _ContactViewState extends State<ContactView> {
icon: FontAwesomeIcons.pencil, icon: FontAwesomeIcons.pencil,
text: context.lang.contactNickname, text: context.lang.contactNickname,
onTap: () async { onTap: () async {
String? newUsername = String? nickName =
await showNicknameChangeDialog(context, contact); await showNicknameChangeDialog(context, contact);
if (newUsername != null && newUsername != "") {
await DbContacts.changeDisplayName( if (context.mounted && nickName != null && nickName != "") {
contact.userId.toInt(), newUsername); final update = ContactsCompanion(nickName: Value(nickName));
context.db.updateContact(contact.userId, update);
} }
}, },
), ),
@ -98,12 +99,15 @@ class _ContactViewState extends State<ContactView> {
onTap: () async { onTap: () async {
bool block = await showAlertDialog( bool block = await showAlertDialog(
context, context,
context.lang.contactBlockTitle(contact.displayName), context.lang
.contactBlockTitle(getContactDisplayName(contact)),
context.lang.contactBlockBody, context.lang.contactBlockBody,
); );
if (block) { if (block) {
await DbContacts.blockUser(contact.userId.toInt()); final update = ContactsCompanion(blocked: Value(true));
// go back to the first if (context.mounted) {
await context.db.updateContact(contact.userId, update);
}
if (context.mounted) { if (context.mounted) {
Navigator.popUntil(context, (route) => route.isFirst); Navigator.popUntil(context, (route) => route.isFirst);
} }
@ -111,6 +115,8 @@ class _ContactViewState extends State<ContactView> {
}, },
), ),
], ],
);
},
), ),
); );
} }

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/settings/privacy_view_block_users.dart'; import 'package:twonly/src/views/settings/privacy_view_block_users.dart';
@ -11,17 +10,9 @@ class PrivacyView extends StatefulWidget {
} }
class _PrivacyViewState extends State<PrivacyView> { class _PrivacyViewState extends State<PrivacyView> {
List<Contact> blockedUsers = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
updateBlockedUsers();
}
Future updateBlockedUsers() async {
blockedUsers = await DbContacts.getBlockedUsers();
setState(() {});
} }
@override @override
@ -34,15 +25,25 @@ class _PrivacyViewState extends State<PrivacyView> {
children: [ children: [
ListTile( ListTile(
title: Text(context.lang.settingsPrivacyBlockUsers), title: Text(context.lang.settingsPrivacyBlockUsers),
subtitle: Text( subtitle: StreamBuilder(
context.lang.settingsPrivacyBlockUsersCount(blockedUsers.length), stream: context.db.watchContactsBlocked(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Text(
context.lang.settingsPrivacyBlockUsersCount(snapshot.data!),
);
} else {
return Text(
context.lang.settingsPrivacyBlockUsersCount(0),
);
}
},
), ),
onTap: () async { onTap: () async {
await Navigator.push(context, await Navigator.push(context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return PrivacyViewBlockUsers(); return PrivacyViewBlockUsers();
})); }));
updateBlockedUsers();
}, },
), ),
], ],

View file

@ -1,6 +1,8 @@
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class PrivacyViewBlockUsers extends StatefulWidget { class PrivacyViewBlockUsers extends StatefulWidget {
@ -11,35 +13,34 @@ class PrivacyViewBlockUsers extends StatefulWidget {
} }
class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> { class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
List<Contact> allUsers = []; late Stream<List<Contact>> allUsers;
List<Contact> filteredUsers = []; List<Contact> filteredUsers = [];
String lastQuery = ""; String filter = "";
@override @override
void initState() { void initState() {
super.initState(); super.initState();
allUsers = context.db.watchAllContacts();
loadAsync(); loadAsync();
} }
Future loadAsync() async { Future loadAsync() async {
allUsers = await DbContacts.getAllUsers();
_filterUsers(lastQuery);
setState(() {}); setState(() {});
} }
Future _filterUsers(String query) async { // Future _filterUsers(String query) async {
lastQuery = query; // lastQuery = query;
if (query.isEmpty) { // if (query.isEmpty) {
filteredUsers = allUsers; // filteredUsers = allUsers;
setState(() {}); // setState(() {});
return; // return;
} // }
filteredUsers = allUsers // filteredUsers = allUsers
.where((user) => // .where((user) =>
user.displayName.toLowerCase().contains(query.toLowerCase())) // user.displayName.toLowerCase().contains(query.toLowerCase()))
.toList(); // .toList();
setState(() {}); // setState(() {});
} // }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -54,7 +55,9 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10), padding: EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: TextField(
onChanged: _filterUsers, onChanged: (value) => setState(() {
filter = value;
}),
decoration: getInputDecoration( decoration: getInputDecoration(
context, context,
context.lang.searchUsernameInput, context.lang.searchUsernameInput,
@ -68,10 +71,22 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Expanded( Expanded(
child: UserList( child: StreamBuilder(
List.from(filteredUsers), stream: allUsers,
updateStatus: () { builder: (context, snapshot) {
loadAsync(); if (!snapshot.hasData) {
return Container();
}
final filteredContacts = snapshot.data!.where((contact) {
return getContactDisplayName(contact)
.toLowerCase()
.contains(filter.toLowerCase());
}).toList();
return UserList(
List.from(filteredContacts),
);
}, },
), ),
) )
@ -83,20 +98,21 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
} }
class UserList extends StatelessWidget { class UserList extends StatelessWidget {
const UserList(this.users, {super.key, required this.updateStatus}); const UserList(this.users, {super.key});
final List<Contact> users; final List<Contact> users;
final Function updateStatus;
Future block(bool? value, int userId) async { Future block(BuildContext context, int userId, bool? value) async {
if (value == null) return; if (value != null) {
await DbContacts.blockUser(userId, unblock: !value); final update = ContactsCompanion(blocked: Value(!value));
updateStatus(); await context.db.updateContact(userId, update);
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Step 1: Sort the users alphabetically // Step 1: Sort the users alphabetically
users.sort((a, b) => a.displayName.compareTo(b.displayName)); users.sort(
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)));
return ListView.builder( return ListView.builder(
restorationId: 'new_message_users_list', restorationId: 'new_message_users_list',
@ -105,20 +121,20 @@ class UserList extends StatelessWidget {
Contact user = users[i]; Contact user = users[i];
return ListTile( return ListTile(
title: Row(children: [ title: Row(children: [
Text(user.displayName), Text(getContactDisplayName(user)),
]), ]),
leading: InitialsAvatar( leading: InitialsAvatar(
displayName: user.displayName, getContactDisplayName(user),
fontSize: 15, fontSize: 15,
), ),
trailing: Checkbox( trailing: Checkbox(
value: user.blocked, value: user.blocked,
onChanged: (bool? value) { onChanged: (bool? value) {
block(value, user.userId.toInt()); block(context, user.userId, value);
}, },
), ),
onTap: () { onTap: () {
block(!user.blocked, user.userId.toInt()); block(context, user.userId, !user.blocked);
}, },
); );
}, },

View file

@ -46,7 +46,7 @@ class _ProfileViewState extends State<ProfileView> {
child: Row( child: Row(
children: [ children: [
InitialsAvatar( InitialsAvatar(
displayName: userData!.username, userData!.username,
fontSize: 30, fontSize: 30,
), ),
SizedBox(width: 20), SizedBox(width: 20),

View file

@ -93,18 +93,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_resolvers name: build_resolvers
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.3" version: "2.4.4"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.14" version: "2.4.15"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
@ -153,6 +153,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -273,6 +281,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
drift:
dependency: "direct main"
description:
name: drift
sha256: "97d5832657d49f26e7a8e07de397ddc63790b039372878d5117af816d0fdb5cb"
url: "https://pub.dev"
source: hosted
version: "2.25.1"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: f1db88482dbb016b9bbddddf746d5d0a6938b156ff20e07320052981f97388cc
url: "https://pub.dev"
source: hosted
version: "2.25.2"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
url: "https://pub.dev"
source: hosted
version: "0.2.4"
ed25519_edwards: ed25519_edwards:
dependency: transitive dependency: transitive
description: description:
@ -1133,6 +1165,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
reorderables: reorderables:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1290,6 +1330,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0+1" version: "3.1.0+1"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
url: "https://pub.dev"
source: hosted
version: "2.7.4"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
url: "https://pub.dev"
source: hosted
version: "0.5.31"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
url: "https://pub.dev"
source: hosted
version: "0.41.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:

View file

@ -16,6 +16,8 @@ dependencies:
collection: ^1.18.0 collection: ^1.18.0
connectivity_plus: ^6.1.2 connectivity_plus: ^6.1.2
cv: ^1.1.3 cv: ^1.1.3
drift: ^2.25.1
drift_flutter: ^0.2.4
exif: ^3.3.0 exif: ^3.3.0
firebase_core: ^3.11.0 firebase_core: ^3.11.0
firebase_messaging: ^15.2.2 firebase_messaging: ^15.2.2
@ -60,10 +62,11 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.3.3 build_runner: ^2.4.15
json_serializable: ^6.8.0 json_serializable: ^6.8.0
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
drift_dev: ^2.25.2
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true