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/db_provider.dart';
late DbProvider dbProvider;
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:provider/provider.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_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:twonly/src/providers/download_change_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/db_provider.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/providers/settings_change_provider.dart';
import 'package:twonly/src/services/fcm_service.dart';
@ -49,9 +47,10 @@ void main() async {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => MessagesChangeProvider()),
ChangeNotifierProvider(create: (_) => DownloadChangeProvider()),
ChangeNotifierProvider(create: (_) => ContactChangeProvider()),
Provider<TwonlyDatabase>(
create: (context) => TwonlyDatabase(),
dispose: (context, db) => db.close(),
),
ChangeNotifierProvider(create: (_) => SendNextMediaTo()),
ChangeNotifierProvider(create: (_) => settingsController),
],

View file

@ -1,9 +1,6 @@
import 'package:provider/provider.dart';
import 'package:twonly/globals.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/utils/storage.dart';
import 'package:twonly/src/views/onboarding/onboarding_view.dart';
@ -23,9 +20,6 @@ Function(bool) globalCallbackConnectionState = (a) {};
bool globalIsAppInBackground = true;
// 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.
class MyApp extends StatefulWidget {
@ -45,10 +39,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
globalIsAppInBackground = false;
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
globalCallbackConnectionState = (isConnected) {
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((_) {
// _requestPermissions();
// _initService();
@ -124,8 +100,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
if (wasPaused) {
globalIsAppInBackground = false;
apiProvider.connect();
context.read<ContactChangeProvider>().update();
context.read<MessagesChangeProvider>().init();
// _stopService();
}
} else if (state == AppLifecycleState.paused) {
@ -145,9 +119,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
WidgetsBinding.instance.removeObserver(this);
// disable globalCallbacks to the flutter tree
globalCallbackConnectionState = (a) {};
globalCallBackOnDownloadChange = (a, b) {};
globalCallBackOnContactChange = () {};
globalCallBackOnMessageChange = (a, b) async {};
super.dispose();
}

View file

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

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.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 {
final Contact user;

View file

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

View file

@ -1,9 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.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/messages_model.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/utils/misc.dart';
enum MessageSendState {
@ -15,21 +15,75 @@ enum MessageSendState {
sending,
}
class MessageSendStateIcon extends StatelessWidget {
final DbMessage message;
MessageSendState messageSendStateFromMessage(Message msg) {
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;
const MessageSendStateIcon(this.message,
const MessageSendStateIcon(this.messages,
{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
Widget build(BuildContext context) {
Widget icon = Placeholder();
String text = "";
Color color =
message.messageContent.getColor(Theme.of(context).colorScheme.primary);
Widget loaderIcon = Row(
children: [
SizedBox(
@ -41,7 +95,9 @@ class MessageSendStateIcon extends StatelessWidget {
],
);
switch (message.getSendState()) {
MessageSendState state = messageSendStateFromMessage(message);
switch (state) {
case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color);
text = context.lang.messageSendState_Received;
@ -65,24 +121,16 @@ class MessageSendStateIcon extends StatelessWidget {
break;
}
if (!message.isDownloaded) {
if (message.downloadState == DownloadState.pending) {
text = context.lang.messageSendState_TapToLoad;
}
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) {
if (message.downloadState == DownloadState.downloaded) {
text = context.lang.messageSendState_Loading;
icon = loaderIcon;
}
return Row(
mainAxisAlignment: mainAxisAlignment,
mainAxisAlignment: widget.mainAxisAlignment,
children: [
icon,
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:pie_menu/pie_menu.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/utils/misc.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/home_view.dart';
class UserContextMenu extends StatefulWidget {
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
State<UserContextMenu> createState() => _UserContextMenuState();
@ -22,38 +24,38 @@ class _UserContextMenuState extends State<UserContextMenu> {
@override
Widget build(BuildContext context) {
return PieMenu(
onPressed: () => print('pressed'),
onPressed: () => (),
actions: [
PieAction(
tooltip: const Text('Verify user'),
tooltip: Text(context.lang.contextMenuVerifyUser),
onSelect: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ContactVerifyView(widget.user);
return ContactVerifyView(widget.contact);
},
));
},
child: widget.user.verified
child: widget.contact.verified
? FaIcon(FontAwesomeIcons.shieldHeart)
: const Icon(Icons.gpp_maybe_rounded), // Can be any widget
: const Icon(Icons.gpp_maybe_rounded),
),
PieAction(
tooltip: const Text('Open chat'),
tooltip: Text(context.lang.contextMenuOpenChat),
onSelect: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ChatItemDetailsView(user: widget.user);
return ChatItemDetailsView(user: widget.contact);
},
));
},
child: const FaIcon(FontAwesomeIcons.solidComments),
),
PieAction(
tooltip: const Text('Send image'),
tooltip: Text(context.lang.contextMenuSendImage),
onSelect: () {
context
.read<SendNextMediaTo>()
.updateSendNextMediaTo(widget.user.userId.toInt());
.updateSendNextMediaTo(widget.contact.userId.toInt());
globalUpdateOfHomeViewPageIndex(0);
},
child: const FaIcon(FontAwesomeIcons.camera),

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.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 {
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!",
"chatListViewSendFirstTwonly": "Sende dein erstes twonly!",
"chatListDetailInput": "Nachricht eingeben",
"contextMenuVerifyUser": "Kontakt verifizieren",
"contextMenuOpenChat": "Chat öffnen",
"contextMenuSendImage": "Bild senden",
"messageSendState_Received": "Empfangen",
"messageSendState_Opened": "Geöffnet",
"messageSendState_Send": "Gesendet",

View file

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

View file

@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
enum MessageKind {
textMessage,
image,
video,
media,
contactRequest,
rejectRequest,
acceptRequest,
@ -11,63 +10,13 @@ enum MessageKind {
ack
}
extension MessageKindExtension on MessageKind {
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 getMessageColorFromType(MessageJson msg, Color primary) {
Color color;
if (this is TextMessageContent) {
final content = msg.content;
if (content is TextMessageContent) {
color = Colors.lightBlue;
} else {
final content = this;
if (content is MediaMessageContent) {
if (content.isRealTwonly) {
color = primary;
@ -83,16 +32,64 @@ class MessageContent {
}
}
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) {
switch (json['type']) {
case 'MediaMessageContent':
static MessageJson fromJson(Map<String, dynamic> json) {
final kind = MessageKindExtension.fromString(json["kind"]);
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);
case 'TextMessageContent':
case MessageKind.textMessage:
return TextMessageContent.fromJson(json);
default:
return MessageContent();
return null;
}
}
@ -125,7 +122,6 @@ class MediaMessageContent extends MessageContent {
@override
Map toJson() {
return {
'type': 'MediaMessageContent',
'downloadToken': downloadToken,
'isRealTwonly': isRealTwonly,
'maxShowTime': maxShowTime,
@ -146,7 +142,6 @@ class TextMessageContent extends MessageContent {
@override
Map toJson() {
return {
'type': 'TextMessageContent',
'text': text,
};
}

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ part of 'user_data.dart';
// **************************************************************************
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,
displayName: json['displayName'] as String,
);
@ -15,5 +15,5 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username,
'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:twonly/globals.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/messages_model.dart';
import '../../../../.blocked/archives/messages_model.dart';
import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/utils/misc.dart';
@ -30,7 +30,7 @@ Future tryTransmitMessages() async {
Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
if (bytes != null) {
Result resp = await apiProvider.sendTextMessage(
Int64(retransmit[i].otherUserId),
retransmit[i].otherUserId,
bytes,
);
@ -46,7 +46,7 @@ Future tryTransmitMessages() async {
if (encryptedMedia != null) {
final content = retransmit[i].messageContent;
if (content is MediaMessageContent) {
uploadMediaFile(msgId, Int64(retransmit[i].otherUserId), encryptedMedia,
uploadMediaFile(msgId, retransmit[i].otherUserId, encryptedMedia,
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
Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
Future<Result> encryptAndSendMessage(int userId, Message msg) async {
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
if (bytes == null) {
@ -79,7 +79,7 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
return resp;
}
Future sendTextMessage(Int64 target, String message) async {
Future sendTextMessage(int target, String message) async {
MessageContent content = TextMessageContent(text: message);
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
Future uploadMediaFile(
int messageId,
Int64 target,
int target,
Uint8List encryptedMedia,
bool isRealTwonly,
int maxShowTime,
@ -179,7 +179,7 @@ Future uploadMediaFile(
}
class SendImage {
final Int64 userId;
final int userId;
final Uint8List imageBytes;
final bool isRealTwonly;
final int maxShowTime;
@ -231,7 +231,7 @@ class SendImage {
}
Future sendImage(
List<Int64> userIds,
List<int> userIds,
Uint8List imageBytes,
bool isRealTwonly,
int maxShowTime,

View file

@ -5,9 +5,9 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.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/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.pbserver.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");
if (fromUserId != null) {
Uint8List? rawBytes =
await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId));
await SignalHelper.decryptBytes(downloadedBytes, fromUserId);
if (rawBytes != null) {
box.put("${data.uploadToken}_downloaded", rawBytes);

View file

@ -256,7 +256,7 @@ class ApiProvider {
var open = Handshake_OpenSession()
..response = signature
..userId = userData.userId;
..userId = Int64(userData.userId);
var opensession = Handshake()..opensession = open;
@ -304,8 +304,8 @@ class ApiProvider {
return await _sendRequestV0(req);
}
Future<Result> getUsername(Int64 userId) async {
var get = ApplicationData_GetUserById()..userId = userId;
Future<Result> getUsername(int userId) async {
var get = ApplicationData_GetUserById()..userId = Int64(userId);
var appData = ApplicationData()..getuserbyid = get;
var req = createClientToServerFromApplicationData(appData);
return await _sendRequestV0(req);
@ -353,9 +353,9 @@ class ApiProvider {
return await _sendRequestV0(req);
}
Future<Result> sendTextMessage(Int64 target, Uint8List msg) async {
Future<Result> sendTextMessage(int target, Uint8List msg) async {
var testMessage = ApplicationData_TextMessage()
..userId = target
..userId = Int64(target)
..body = msg;
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:math';
import 'package:twonly/src/model/contacts_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/sender_key_store_model.dart';
import 'package:twonly/src/model/session_store_model.dart';
@ -55,8 +53,6 @@ class DbProvider {
await DbSignalPreKeyStore.setupDatabaseTable(db);
await DbSignalSenderKeyStore.setupDatabaseTable(db);
await DbSignalIdentityKeyStore.setupDatabaseTable(db);
await DbContacts.setupDatabaseTable(db);
await DbMessages.setupDatabaseTable(db);
}
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:twonly/globals.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/db_provider.dart';
import 'package:twonly/src/utils/misc.dart';
import '../../firebase_options.dart';
@ -65,33 +65,26 @@ Future initFCMService() async {
});
}
late TwonlyDatabase bgTwonlyDB;
@pragma('vm:entry-point')
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.
// -> 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();
await apiProvider.connect();
final stopwatch = Stopwatch()..start();
while (!gotMessage) {
while (true) {
if (stopwatch.elapsed >= Duration(seconds: 20)) {
Logger("firebase-background").shout('Timeout reached. Exiting the loop.');
break; // Exit the loop if the timeout is reached.
Logger("firebase-background").shout('Exiting background handler');
break;
}
await Future.delayed(Duration(milliseconds: 10));
}

View file

@ -1,10 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.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;
/// 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")) {
pushNotificationText = {
"newTextMessage": "%userName% hat dir eine Nachricht gesendet.",
"newTwonly": "%userName% hat dir einen twonly gesendet.",
"newVideo": "%userName% hat dir eine Video gesendet.",
"newImage": "%userName% hat dir eine Bild gesendet.",
"newTwonly": "%userName% hat dir ein twonly gesendet.",
"newVideo": "%userName% hat dir ein Video gesendet.",
"newImage": "%userName% hat dir ein Bild gesendet.",
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
};
@ -166,7 +165,10 @@ String getPushNotificationText(String key, String userName) {
Future localPushNotificationNewMessage(
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;
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:fixnum/fixnum.dart';
import 'dart:convert';
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> {
const Uint8ListConverter();
@override

View file

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

View file

@ -27,7 +27,7 @@ Future<ECPrivateKey?> getPrivateKey() async {
}
Future<bool> addNewContact(Response_UserData userData) async {
final Int64 userId = userData.userId;
final int userId = userData.userId.toInt();
SignalProtocolAddress targetAddress =
SignalProtocolAddress(userId.toString(), defaultDeviceId);
@ -141,13 +141,14 @@ Future createIfNotExistsSignalIdentity() async {
final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(),
registrationId: Int64(registrationId));
registrationId: registrationId,
);
await storage.write(
key: "signal_identity", value: jsonEncode(storedSignalIdentity));
}
Future<Fingerprint?> generateSessionFingerPrint(Int64 target) async {
Future<Fingerprint?> generateSessionFingerPrint(int target) async {
ConnectSignalProtocolStore? signalStore = await getSignalStore();
UserData? user = await getUser();
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 {
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 {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;

View file

@ -375,7 +375,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (sendNextMediaToUserId != null) {
Uint8List? imageBytes = await getMergedImage();
sendImage(
[Int64(sendNextMediaToUserId)],
[sendNextMediaToUserId],
imageBytes!,
_isRealTwonly,
_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/initialsavatar.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/messages_change_provider.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/message_send_state_icon.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/messages_model.dart';
import '../../../../.blocked/archives/messages_model.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';

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

View file

@ -7,9 +7,9 @@ import 'package:no_screenshot/no_screenshot.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/components/animate_icon.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/messages_model.dart';
import '../../../../.blocked/archives/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';

View file

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

View file

@ -1,12 +1,12 @@
import 'dart:convert';
import 'package:drift/drift.dart' hide Column;
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.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: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/signal.dart';
import 'package:url_launcher/url_launcher.dart';
@ -36,10 +36,9 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
@override
Widget build(BuildContext context) {
Contact contact = context
.watch<ContactChangeProvider>()
.allContacts
.firstWhere((c) => c.userId == widget.contact.userId);
Stream<Contact?> contact = context.db
.getContactByUserId(widget.contact.userId)
.watchSingleOrNull();
return Scaffold(
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),
child: Text(
context.lang
.contactVerifyNumberLongDesc(contact.displayName),
context.lang.contactVerifyNumberLongDesc(
getContactDisplayName(snapshot.data!)),
textAlign: TextAlign.center,
),
);
},
),
Padding(
padding:
@ -125,23 +132,33 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
contact.verified
? OutlinedButton.icon(
StreamBuilder(
stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contact = snapshot.data!;
if (contact.verified) {
return OutlinedButton.icon(
onPressed: () {
DbContacts.updateVerificationStatus(
contact.userId.toInt(), false);
final update =
ContactsCompanion(verified: Value(false));
context.db.updateContact(contact.userId, update);
},
label: Text(
context.lang.contactVerifyNumberClearVerification),
)
: FilledButton.icon(
);
}
return FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.shieldHeart),
onPressed: () {
DbContacts.updateVerificationStatus(
contact.userId.toInt(), true);
final update = ContactsCompanion(verified: Value(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:provider/provider.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/components/better_list_title.dart';
import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/verified_shield.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/providers/contacts_change_provider.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/views/contact/contact_verify_view.dart';
@ -24,26 +23,27 @@ class ContactView extends StatefulWidget {
class _ContactViewState extends State<ContactView> {
@override
Widget build(BuildContext context) {
Contact contact = context
.watch<ContactChangeProvider>()
.allContacts
.firstWhere((c) => c.userId == widget.userId);
int flameCounter = context
.watch<MessagesChangeProvider>()
.flamesCounter[contact.userId.toInt()] ??
0;
Stream<Contact?> contact =
context.db.getContactByUserId(widget.userId).watchSingleOrNull();
return Scaffold(
appBar: AppBar(
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: [
Padding(
padding: const EdgeInsets.all(10),
child: InitialsAvatar(
displayName: contact.displayName,
getContactDisplayName(contact),
fontSize: 30,
),
),
@ -54,7 +54,7 @@ class _ContactViewState extends State<ContactView> {
padding: EdgeInsets.only(right: 10),
child: VerifiedShield(contact)),
Text(
contact.displayName,
getContactDisplayName(contact),
style: TextStyle(fontSize: 20),
),
if (flameCounter > 0)
@ -71,11 +71,12 @@ class _ContactViewState extends State<ContactView> {
icon: FontAwesomeIcons.pencil,
text: context.lang.contactNickname,
onTap: () async {
String? newUsername =
String? nickName =
await showNicknameChangeDialog(context, contact);
if (newUsername != null && newUsername != "") {
await DbContacts.changeDisplayName(
contact.userId.toInt(), newUsername);
if (context.mounted && nickName != null && nickName != "") {
final update = ContactsCompanion(nickName: Value(nickName));
context.db.updateContact(contact.userId, update);
}
},
),
@ -98,12 +99,15 @@ class _ContactViewState extends State<ContactView> {
onTap: () async {
bool block = await showAlertDialog(
context,
context.lang.contactBlockTitle(contact.displayName),
context.lang
.contactBlockTitle(getContactDisplayName(contact)),
context.lang.contactBlockBody,
);
if (block) {
await DbContacts.blockUser(contact.userId.toInt());
// go back to the first
final update = ContactsCompanion(blocked: Value(true));
if (context.mounted) {
await context.db.updateContact(contact.userId, update);
}
if (context.mounted) {
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:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/utils/misc.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> {
List<Contact> blockedUsers = [];
@override
void initState() {
super.initState();
updateBlockedUsers();
}
Future updateBlockedUsers() async {
blockedUsers = await DbContacts.getBlockedUsers();
setState(() {});
}
@override
@ -34,15 +25,25 @@ class _PrivacyViewState extends State<PrivacyView> {
children: [
ListTile(
title: Text(context.lang.settingsPrivacyBlockUsers),
subtitle: Text(
context.lang.settingsPrivacyBlockUsersCount(blockedUsers.length),
subtitle: StreamBuilder(
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 {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return PrivacyViewBlockUsers();
}));
updateBlockedUsers();
},
),
],

View file

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

View file

@ -93,18 +93,18 @@ packages:
dependency: transitive
description:
name: build_resolvers
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
url: "https://pub.dev"
source: hosted
version: "2.4.3"
version: "2.4.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
url: "https://pub.dev"
source: hosted
version: "2.4.14"
version: "2.4.15"
build_runner_core:
dependency: transitive
description:
@ -153,6 +153,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@ -273,6 +281,30 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -1133,6 +1165,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
reorderables:
dependency: "direct main"
description:
@ -1290,6 +1330,30 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View file

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